diff --git a/background.js b/background.js index 11fd5ec..8d5931d 100644 --- a/background.js +++ b/background.js @@ -1,6 +1,6 @@ // background.js — Service Worker: orchestration, state, tab management, message routing -importScripts('data/names.js', 'hotmail-utils.js', 'content/activation-utils.js'); +importScripts('data/names.js', 'hotmail-utils.js', 'luckmail-utils.js', 'content/activation-utils.js'); const { buildHotmailMailApiLatestUrl, @@ -17,6 +17,33 @@ const { pickVerificationMessageWithTimeFallback, shouldClearHotmailCurrentSelection, } = self.HotmailUtils; +const { + DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME, + DEFAULT_LUCKMAIL_BASE_URL, + DEFAULT_LUCKMAIL_EMAIL_TYPE, + buildLuckmailBaselineCursor, + buildLuckmailMailCursor, + filterReusableLuckmailPurchases, + isLuckmailMailNewerThanCursor, + isLuckmailPurchaseReusable, + isLuckmailPurchaseForProject, + isLuckmailPurchasePreserved, + normalizeLuckmailBaseUrl, + normalizeLuckmailEmailType, + normalizeLuckmailMailCursor, + normalizeLuckmailProjectName, + normalizeLuckmailPurchase, + normalizeLuckmailPurchaseId, + normalizeLuckmailPurchaseListPage, + normalizeLuckmailPurchases, + normalizeLuckmailTags, + normalizeLuckmailTokenCode, + normalizeLuckmailTokenMail, + normalizeLuckmailTokenMails, + normalizeLuckmailUsedPurchases, + normalizeTimestamp: normalizeLuckmailTimestamp, + pickLuckmailVerificationMail, +} = self.LuckMailUtils; const { isRecoverableStep9AuthFailure, } = self.MultiPageActivationUtils; @@ -24,6 +51,7 @@ const { const LOG_PREFIX = '[MultiPage:bg]'; const DUCK_AUTOFILL_URL = 'https://duckduckgo.com/email/settings/autofill'; const HOTMAIL_PROVIDER = 'hotmail-api'; +const LUCKMAIL_PROVIDER = 'luckmail-api'; const HOTMAIL_MAILBOXES = ['INBOX', 'Junk']; const STOP_ERROR_MESSAGE = '流程已被用户停止。'; const HUMAN_STEP_DELAY_MIN = 700; @@ -48,6 +76,7 @@ const HOTMAIL_SERVICE_MODE_LOCAL = 'local'; 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 DEFAULT_LUCKMAIL_PROJECT_CODE = 'openai'; const DISPLAY_TIMEZONE = 'Asia/Shanghai'; initializeSessionStorageAccess(); @@ -111,6 +140,15 @@ const DEFAULT_STATE = { sourceLastUrls: {}, // 各来源页面最近一次打开的地址记录。 logs: [], // 侧边栏展示的运行日志。 ...PERSISTED_SETTING_DEFAULTS, // 合并 chrome.storage.local 中持久化保存的用户配置。 + luckmailApiKey: '', + luckmailBaseUrl: DEFAULT_LUCKMAIL_BASE_URL, + luckmailEmailType: DEFAULT_LUCKMAIL_EMAIL_TYPE, + luckmailDomain: '', + luckmailUsedPurchases: {}, + luckmailPreserveTagId: 0, + luckmailPreserveTagName: DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME, + currentLuckmailPurchase: null, + currentLuckmailMailCursor: null, autoRunning: false, // 当前是否处于自动运行中。 autoRunPhase: 'idle', // 当前自动运行阶段。 autoRunCurrentRun: 0, // 自动运行当前执行到第几轮。 @@ -233,6 +271,7 @@ function normalizeMailProvider(value = '') { switch (normalized) { case 'custom': case HOTMAIL_PROVIDER: + case LUCKMAIL_PROVIDER: case '163': case '163-vip': case 'qq': @@ -244,6 +283,48 @@ function normalizeMailProvider(value = '') { } } +function buildLuckmailSessionSettingsPayload(input = {}) { + if (!input || typeof input !== 'object' || Array.isArray(input)) { + return {}; + } + + const payload = {}; + + if (input.luckmailApiKey !== undefined) { + payload.luckmailApiKey = String(input.luckmailApiKey || ''); + } + if (input.luckmailBaseUrl !== undefined) { + payload.luckmailBaseUrl = normalizeLuckmailBaseUrl(input.luckmailBaseUrl); + } + if (input.luckmailEmailType !== undefined) { + payload.luckmailEmailType = normalizeLuckmailEmailType(input.luckmailEmailType); + } + if (input.luckmailDomain !== undefined) { + payload.luckmailDomain = String(input.luckmailDomain || '').trim(); + } + if (input.luckmailUsedPurchases !== undefined) { + payload.luckmailUsedPurchases = normalizeLuckmailUsedPurchases(input.luckmailUsedPurchases); + } + if (input.luckmailPreserveTagId !== undefined) { + payload.luckmailPreserveTagId = Number(input.luckmailPreserveTagId) || 0; + } + if (input.luckmailPreserveTagName !== undefined) { + payload.luckmailPreserveTagName = String(input.luckmailPreserveTagName || '').trim() || DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME; + } + if (input.currentLuckmailPurchase !== undefined) { + payload.currentLuckmailPurchase = input.currentLuckmailPurchase + ? normalizeLuckmailPurchase(input.currentLuckmailPurchase) + : null; + } + if (input.currentLuckmailMailCursor !== undefined) { + payload.currentLuckmailMailCursor = input.currentLuckmailMailCursor + ? normalizeLuckmailMailCursor(input.currentLuckmailMailCursor) + : null; + } + + return payload; +} + function normalizeLocalCpaStep9Mode(value = '') { return String(value || '').trim().toLowerCase() === 'bypass' ? 'bypass' @@ -551,6 +632,86 @@ async function setPasswordState(password) { broadcastDataUpdate({ password }); } +function getLuckmailUsedPurchases(state = {}) { + return normalizeLuckmailUsedPurchases(state?.luckmailUsedPurchases); +} + +function getLuckmailPreserveTagInfo(state = {}) { + return { + id: Number(state?.luckmailPreserveTagId) || 0, + name: String(state?.luckmailPreserveTagName || '').trim() || DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME, + }; +} + +async function setLuckmailUsedPurchasesState(usedPurchases) { + const normalizedUsedPurchases = normalizeLuckmailUsedPurchases(usedPurchases); + await setState({ luckmailUsedPurchases: normalizedUsedPurchases }); + broadcastDataUpdate({ luckmailUsedPurchases: normalizedUsedPurchases }); + return normalizedUsedPurchases; +} + +async function setLuckmailPurchaseUsedState(purchaseId, used) { + const normalizedPurchaseId = normalizeLuckmailPurchaseId(purchaseId); + if (!normalizedPurchaseId) { + throw new Error('LuckMail 邮箱 ID 无效。'); + } + + const state = await getState(); + const usedPurchases = getLuckmailUsedPurchases(state); + if (used) { + usedPurchases[normalizedPurchaseId] = true; + } else { + delete usedPurchases[normalizedPurchaseId]; + } + + await setLuckmailUsedPurchasesState(usedPurchases); + return { + purchaseId: Number(normalizedPurchaseId), + used: Boolean(used), + }; +} + +async function setLuckmailPreserveTagInfo(tag) { + const normalizedTags = normalizeLuckmailTags([tag]); + const normalizedTag = normalizedTags[0] || { + id: 0, + name: DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME, + }; + const updates = { + luckmailPreserveTagId: Number(normalizedTag.id) || 0, + luckmailPreserveTagName: String(normalizedTag.name || '').trim() || DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME, + }; + await setState(updates); + broadcastDataUpdate(updates); + return updates; +} + +async function setLuckmailPurchaseState(purchase) { + const normalizedPurchase = purchase ? normalizeLuckmailPurchase(purchase) : null; + await setState({ currentLuckmailPurchase: normalizedPurchase }); + broadcastDataUpdate({ currentLuckmailPurchase: normalizedPurchase }); + return normalizedPurchase; +} + +async function setLuckmailMailCursorState(cursor) { + const normalizedCursor = cursor ? normalizeLuckmailMailCursor(cursor) : null; + await setState({ currentLuckmailMailCursor: normalizedCursor }); + return normalizedCursor; +} + +async function clearLuckmailRuntimeState(options = {}) { + const { clearEmail = false } = options; + const updates = { + currentLuckmailPurchase: null, + currentLuckmailMailCursor: null, + }; + if (clearEmail) { + updates.email = null; + } + await setState(updates); + broadcastDataUpdate(updates); +} + async function resetState() { console.log(LOG_PREFIX, 'Resetting all state'); // Preserve settings and persistent data across resets @@ -561,6 +722,13 @@ async function resetState() { 'accounts', 'tabRegistry', 'sourceLastUrls', + 'luckmailApiKey', + 'luckmailBaseUrl', + 'luckmailEmailType', + 'luckmailDomain', + 'luckmailUsedPurchases', + 'luckmailPreserveTagId', + 'luckmailPreserveTagName', ]), getPersistedSettings(), ]); @@ -573,6 +741,15 @@ async function resetState() { accounts: prev.accounts || [], tabRegistry: prev.tabRegistry || {}, sourceLastUrls: prev.sourceLastUrls || {}, + luckmailApiKey: String(prev.luckmailApiKey || ''), + luckmailBaseUrl: normalizeLuckmailBaseUrl(prev.luckmailBaseUrl), + luckmailEmailType: normalizeLuckmailEmailType(prev.luckmailEmailType), + luckmailDomain: String(prev.luckmailDomain || '').trim(), + luckmailUsedPurchases: normalizeLuckmailUsedPurchases(prev.luckmailUsedPurchases), + luckmailPreserveTagId: Number(prev.luckmailPreserveTagId) || 0, + luckmailPreserveTagName: String(prev.luckmailPreserveTagName || '').trim() || DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME, + currentLuckmailPurchase: null, + currentLuckmailMailCursor: null, }); } @@ -646,6 +823,13 @@ function isHotmailProvider(stateOrProvider) { return provider === HOTMAIL_PROVIDER; } +function isLuckmailProvider(stateOrProvider) { + const provider = typeof stateOrProvider === 'string' + ? stateOrProvider + : stateOrProvider?.mailProvider; + return provider === LUCKMAIL_PROVIDER; +} + function isCustomMailProvider(stateOrProvider) { const provider = typeof stateOrProvider === 'string' ? stateOrProvider @@ -1302,6 +1486,703 @@ function buildGeneratedAliasEmail(state) { throw new Error(`未支持的别名邮箱类型:${provider}`); } +function getLuckmailSessionConfig(state = {}) { + return { + apiKey: String(state.luckmailApiKey || ''), + baseUrl: normalizeLuckmailBaseUrl(state.luckmailBaseUrl), + emailType: normalizeLuckmailEmailType(state.luckmailEmailType), + domain: String(state.luckmailDomain || '').trim(), + }; +} + +function ensureLuckmailApiKey(state = {}) { + const apiKey = String(state.luckmailApiKey || '').trim(); + if (!apiKey) { + throw new Error('LuckMail API Key 为空,请先在侧边栏填写。'); + } + return apiKey; +} + +async function requestLuckmail(method, path, { baseUrl, apiKey, params, jsonData, timeout = 30000 } = {}) { + const requestUrl = new URL(`${normalizeLuckmailBaseUrl(baseUrl)}${path}`); + if (params && typeof params === 'object') { + for (const [key, value] of Object.entries(params)) { + if (value === undefined || value === null || value === '') continue; + requestUrl.searchParams.set(key, String(value)); + } + } + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + const headers = { + Accept: 'application/json', + }; + if (apiKey) { + headers['X-API-Key'] = apiKey; + } + + const upperMethod = String(method || 'GET').toUpperCase(); + const fetchOptions = { + method: upperMethod, + headers, + signal: controller.signal, + }; + if (jsonData !== undefined) { + headers['Content-Type'] = 'application/json'; + fetchOptions.body = JSON.stringify(jsonData || {}); + } + + let response = null; + try { + response = await fetch(requestUrl.toString(), fetchOptions); + } catch (err) { + if (err?.name === 'AbortError') { + throw new Error(`LuckMail 请求超时:${path}`); + } + throw new Error(`LuckMail 请求失败:${err.message}`); + } finally { + clearTimeout(timeoutId); + } + + let payload = null; + try { + payload = await response.json(); + } catch { + throw new Error(`LuckMail 返回了无法解析的响应:${path}`); + } + + if (!response.ok) { + const errorText = String(payload?.message || response.statusText || 'HTTP error'); + throw new Error(`LuckMail 请求失败:${errorText}`); + } + + if (!payload || typeof payload !== 'object') { + throw new Error(`LuckMail 返回数据无效:${path}`); + } + + if (payload.code !== 0) { + const errorText = String(payload.message || 'Unknown error'); + throw new Error(`LuckMail 接口返回失败:${errorText}`); + } + + return payload.data; +} + +function createLuckmailClient(state = {}) { + const config = getLuckmailSessionConfig(state); + const apiKey = ensureLuckmailApiKey(state); + const request = (method, path, options = {}) => requestLuckmail(method, path, { + baseUrl: config.baseUrl, + apiKey, + ...options, + }); + + return { + user: { + async purchaseEmails(projectCode, quantity, { emailType, domain } = {}) { + const body = { + project_code: projectCode, + quantity, + email_type: normalizeLuckmailEmailType(emailType), + }; + if (domain) { + body.domain = String(domain).trim(); + } + return request('POST', '/api/v1/openapi/email/purchase', { + jsonData: body, + }); + }, + async getPurchases({ page = 1, pageSize = 100, projectId, tagId, keyword, userDisabled } = {}) { + return normalizeLuckmailPurchaseListPage(await request('GET', '/api/v1/openapi/email/purchases', { + params: { + page, + page_size: pageSize, + project_id: projectId, + tag_id: tagId, + keyword, + user_disabled: userDisabled, + }, + })); + }, + async getTokenCode(token) { + return normalizeLuckmailTokenCode(await request( + 'GET', + `/api/v1/openapi/email/token/${encodeURIComponent(token)}/code` + )); + }, + async checkTokenAlive(token) { + const data = await request( + 'GET', + `/api/v1/openapi/email/token/${encodeURIComponent(token)}/alive` + ); + return { + email_address: String(data?.email_address || ''), + project: String(data?.project || ''), + alive: Boolean(data?.alive), + status: String(data?.status || ''), + message: String(data?.message || ''), + mail_count: Number(data?.mail_count) || 0, + }; + }, + async getTokenMails(token) { + const data = await request('GET', `/api/v1/openapi/email/token/${encodeURIComponent(token)}/mails`); + return { + email_address: String(data?.email_address || ''), + project: String(data?.project || ''), + warranty_until: String(data?.warranty_until || ''), + mails: normalizeLuckmailTokenMails(data?.mails || []), + }; + }, + async getTokenMailDetail(token, messageId) { + return normalizeLuckmailTokenMail(await request( + 'GET', + `/api/v1/openapi/email/token/${encodeURIComponent(token)}/mails/${encodeURIComponent(messageId)}` + )); + }, + async setPurchaseDisabled(purchaseId, disabled) { + await request('PUT', `/api/v1/openapi/email/purchases/${encodeURIComponent(purchaseId)}/disabled`, { + jsonData: { + disabled: disabled ? 1 : 0, + }, + }); + }, + async batchSetPurchaseDisabled(ids, disabled) { + await request('POST', '/api/v1/openapi/email/purchases/batch-disabled', { + jsonData: { + ids: (Array.isArray(ids) ? ids : []).map((id) => Number(id)).filter((id) => Number.isFinite(id) && id > 0), + disabled: disabled ? 1 : 0, + }, + }); + }, + async setPurchaseTag(purchaseId, { tagId, tagName } = {}) { + const body = {}; + if (tagId !== undefined) { + body.tag_id = Number(tagId) || 0; + } + if (tagName !== undefined) { + body.tag_name = String(tagName || '').trim(); + } + await request('PUT', `/api/v1/openapi/email/purchases/${encodeURIComponent(purchaseId)}/tag`, { + jsonData: body, + }); + }, + async batchSetPurchaseTag(ids, { tagId, tagName } = {}) { + const body = { + ids: (Array.isArray(ids) ? ids : []).map((id) => Number(id)).filter((id) => Number.isFinite(id) && id > 0), + }; + if (tagId !== undefined) { + body.tag_id = Number(tagId) || 0; + } + if (tagName !== undefined) { + body.tag_name = String(tagName || '').trim(); + } + await request('POST', '/api/v1/openapi/email/purchases/batch-tag', { + jsonData: body, + }); + }, + async getTags() { + return normalizeLuckmailTags(await request('GET', '/api/v1/openapi/email/tags')); + }, + async createTag(name, limitType, remark) { + const body = { + name: String(name || '').trim(), + limit_type: Number(limitType) || 0, + }; + if (remark !== undefined) { + body.remark = String(remark || '').trim(); + } + return normalizeLuckmailTags([await request('POST', '/api/v1/openapi/email/tags', { + jsonData: body, + })])[0] || null; + }, + }, + }; +} + +function getCurrentLuckmailPurchase(state = {}) { + return state.currentLuckmailPurchase + ? normalizeLuckmailPurchase(state.currentLuckmailPurchase) + : null; +} + +function buildLuckmailPurchaseView(purchase, state = {}) { + const normalizedPurchase = normalizeLuckmailPurchase(purchase); + const usedPurchases = getLuckmailUsedPurchases(state); + const preserveTagInfo = getLuckmailPreserveTagInfo(state); + + return { + id: normalizedPurchase.id, + email_address: normalizedPurchase.email_address, + project_name: normalizeLuckmailProjectName(normalizedPurchase.project_name) || DEFAULT_LUCKMAIL_PROJECT_CODE, + price: normalizedPurchase.price, + status: normalizedPurchase.status, + tag_id: normalizedPurchase.tag_id, + tag_name: normalizedPurchase.tag_name, + user_disabled: normalizedPurchase.user_disabled, + warranty_hours: normalizedPurchase.warranty_hours, + warranty_until: normalizedPurchase.warranty_until, + created_at: normalizedPurchase.created_at, + used: Boolean(usedPurchases[normalizeLuckmailPurchaseId(normalizedPurchase.id)]), + preserved: isLuckmailPurchasePreserved(normalizedPurchase, { + preserveTagId: preserveTagInfo.id, + preserveTagName: preserveTagInfo.name, + }), + disabled: normalizedPurchase.user_disabled === 1, + current: Number(getCurrentLuckmailPurchase(state)?.id) === normalizedPurchase.id, + reusable: isLuckmailPurchaseReusable(normalizedPurchase, { + projectCode: DEFAULT_LUCKMAIL_PROJECT_CODE, + usedPurchases, + preserveTagId: preserveTagInfo.id, + preserveTagName: preserveTagInfo.name, + now: Date.now(), + }), + }; +} + +async function getAllLuckmailPurchases(state, options = {}) { + const client = options.client || createLuckmailClient(state); + const pageSize = Math.max(1, Math.min(100, Number(options.pageSize) || 100)); + const maxPages = Math.max(1, Number(options.maxPages) || 50); + const purchases = []; + + for (let page = 1; page <= maxPages; page += 1) { + const pageResult = await client.user.getPurchases({ + page, + pageSize, + keyword: options.keyword, + projectId: options.projectId, + tagId: options.tagId, + userDisabled: options.userDisabled, + }); + const normalizedPage = normalizeLuckmailPurchaseListPage(pageResult); + purchases.push(...normalizedPage.list); + + if (normalizedPage.list.length === 0) { + break; + } + if (normalizedPage.total > 0 && purchases.length >= normalizedPage.total) { + break; + } + if (normalizedPage.list.length < normalizedPage.page_size) { + break; + } + } + + return purchases; +} + +async function listLuckmailPurchasesByProject(state, options = {}) { + const projectCode = normalizeLuckmailProjectName(options.projectCode || DEFAULT_LUCKMAIL_PROJECT_CODE) + || DEFAULT_LUCKMAIL_PROJECT_CODE; + const purchases = await getAllLuckmailPurchases(state, options); + return purchases.filter((purchase) => isLuckmailPurchaseForProject(purchase, projectCode)); +} + +async function getLuckmailPurchaseById(state, purchaseId, options = {}) { + const normalizedPurchaseId = Number(normalizeLuckmailPurchaseId(purchaseId)) || 0; + if (!normalizedPurchaseId) { + throw new Error('LuckMail 邮箱 ID 无效。'); + } + + const purchases = await listLuckmailPurchasesByProject(state, options); + const purchase = purchases.find((item) => item.id === normalizedPurchaseId) || null; + if (!purchase) { + throw new Error(`未找到 ID=${normalizedPurchaseId} 的 openai LuckMail 邮箱。`); + } + return purchase; +} + +async function listLuckmailPurchasesForManagement() { + const state = await getState(); + const purchases = await listLuckmailPurchasesByProject(state, { + projectCode: DEFAULT_LUCKMAIL_PROJECT_CODE, + }); + return purchases.map((purchase) => buildLuckmailPurchaseView(purchase, state)); +} + +async function ensureLuckmailPreserveTag(client, state = null) { + const resolvedState = state || await getState(); + const preserveTagInfo = getLuckmailPreserveTagInfo(resolvedState); + if (preserveTagInfo.id > 0) { + return preserveTagInfo; + } + + const tags = normalizeLuckmailTags(await client.user.getTags()); + let preserveTag = tags.find( + (tag) => normalizeLuckmailProjectName(tag.name) === normalizeLuckmailProjectName(preserveTagInfo.name) + ) || null; + + if (!preserveTag) { + preserveTag = await client.user.createTag( + DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME, + 0, + '保留邮箱(不参与自动复用)' + ); + } + + await setLuckmailPreserveTagInfo(preserveTag); + return { + id: Number(preserveTag?.id) || 0, + name: String(preserveTag?.name || '').trim() || DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME, + }; +} + +async function activateLuckmailPurchaseForFlow(state, client, purchase, options = {}) { + const normalizedPurchase = normalizeLuckmailPurchase(purchase); + if (!normalizedPurchase?.email_address || !normalizedPurchase?.token) { + throw new Error('LuckMail 邮箱缺少 email/token,无法用于当前流程。'); + } + + let baselineCursor = null; + if (options.initializeCursor !== false) { + const mailList = await client.user.getTokenMails(normalizedPurchase.token); + baselineCursor = buildLuckmailBaselineCursor(mailList?.mails || []); + } + + await setLuckmailPurchaseState(normalizedPurchase); + await setLuckmailMailCursorState(baselineCursor); + await setEmailState(normalizedPurchase.email_address); + + if (options.logMessage) { + await addLog(options.logMessage, options.logLevel || 'ok'); + } + + return normalizedPurchase; +} + +async function findReusableLuckmailPurchaseForFlow(state, client) { + const preserveTagInfo = getLuckmailPreserveTagInfo(state); + const reusablePurchases = filterReusableLuckmailPurchases( + await listLuckmailPurchasesByProject(state, { + client, + projectCode: DEFAULT_LUCKMAIL_PROJECT_CODE, + }), + { + projectCode: DEFAULT_LUCKMAIL_PROJECT_CODE, + usedPurchases: getLuckmailUsedPurchases(state), + preserveTagId: preserveTagInfo.id, + preserveTagName: preserveTagInfo.name, + now: Date.now(), + } + ); + + for (const candidate of reusablePurchases) { + try { + const aliveResult = await client.user.checkTokenAlive(candidate.token); + if (!aliveResult?.alive) { + await addLog( + `LuckMail:跳过不可复用邮箱 ${candidate.email_address}:${aliveResult?.message || aliveResult?.status || 'token 不可用'}`, + 'warn' + ); + continue; + } + return candidate; + } catch (err) { + await addLog(`LuckMail:检测复用邮箱 ${candidate.email_address} 失败:${err.message}`, 'warn'); + } + } + + return null; +} + +async function selectLuckmailPurchase(purchaseId) { + const state = await ensureManualInteractionAllowed('切换 LuckMail 邮箱'); + const client = createLuckmailClient(state); + const purchase = await getLuckmailPurchaseById(state, purchaseId, { + client, + projectCode: DEFAULT_LUCKMAIL_PROJECT_CODE, + }); + + if (purchase.user_disabled === 1) { + throw new Error(`LuckMail 邮箱 ${purchase.email_address} 已禁用,无法使用。`); + } + + const aliveResult = await client.user.checkTokenAlive(purchase.token); + if (!aliveResult?.alive) { + throw new Error(`LuckMail 邮箱 ${purchase.email_address} 当前不可用:${aliveResult?.message || aliveResult?.status || 'token 已失效'}`); + } + + const activatedPurchase = await activateLuckmailPurchaseForFlow(state, client, purchase, { + initializeCursor: true, + logMessage: `LuckMail:已切换当前邮箱为 ${purchase.email_address}`, + }); + const nextState = await getState(); + return buildLuckmailPurchaseView(activatedPurchase, nextState); +} + +async function setLuckmailPurchasePreservedState(purchaseId, preserved) { + const state = await ensureManualInteractionAllowed('设置 LuckMail 邮箱保留状态'); + const client = createLuckmailClient(state); + const purchase = await getLuckmailPurchaseById(state, purchaseId, { + client, + projectCode: DEFAULT_LUCKMAIL_PROJECT_CODE, + }); + + if (preserved) { + const preserveTag = await ensureLuckmailPreserveTag(client, state); + await client.user.setPurchaseTag(purchase.id, { tagId: preserveTag.id }); + } else { + await client.user.setPurchaseTag(purchase.id, { tagId: 0 }); + } + + await addLog(`LuckMail:已将 ${purchase.email_address} ${preserved ? '设为保留' : '取消保留'}`, 'ok'); + const refreshedState = await getState(); + const refreshedPurchase = await getLuckmailPurchaseById(refreshedState, purchase.id, { + client, + projectCode: DEFAULT_LUCKMAIL_PROJECT_CODE, + }); + return buildLuckmailPurchaseView(refreshedPurchase, await getState()); +} + +async function setLuckmailPurchaseDisabledState(purchaseId, disabled) { + const state = await ensureManualInteractionAllowed(disabled ? '禁用 LuckMail 邮箱' : '启用 LuckMail 邮箱'); + const client = createLuckmailClient(state); + const purchase = await getLuckmailPurchaseById(state, purchaseId, { + client, + projectCode: DEFAULT_LUCKMAIL_PROJECT_CODE, + }); + + await client.user.setPurchaseDisabled(purchase.id, disabled ? 1 : 0); + + const currentPurchase = getCurrentLuckmailPurchase(await getState()); + if (disabled && currentPurchase?.id === purchase.id) { + await clearLuckmailRuntimeState({ clearEmail: isLuckmailProvider(await getState()) }); + } + + await addLog(`LuckMail:已将 ${purchase.email_address} ${disabled ? '禁用' : '启用'}`, 'ok'); + const refreshedState = await getState(); + const refreshedPurchase = await getLuckmailPurchaseById(refreshedState, purchase.id, { + client, + projectCode: DEFAULT_LUCKMAIL_PROJECT_CODE, + }); + return buildLuckmailPurchaseView(refreshedPurchase, await getState()); +} + +async function batchUpdateLuckmailPurchases(input = {}) { + const action = String(input.action || '').trim(); + const selectedIds = Array.isArray(input.ids) + ? [...new Set(input.ids.map((id) => Number(normalizeLuckmailPurchaseId(id)) || 0).filter((id) => id > 0))] + : []; + if (!selectedIds.length) { + throw new Error('请先选择至少一个 LuckMail 邮箱。'); + } + + const state = await ensureManualInteractionAllowed('批量更新 LuckMail 邮箱'); + const client = createLuckmailClient(state); + const purchases = await listLuckmailPurchasesByProject(state, { + client, + projectCode: DEFAULT_LUCKMAIL_PROJECT_CODE, + }); + const purchaseMap = new Map(purchases.map((purchase) => [purchase.id, purchase])); + const targetPurchases = selectedIds.map((id) => purchaseMap.get(id)).filter(Boolean); + + if (!targetPurchases.length) { + throw new Error('未找到可批量处理的 openai LuckMail 邮箱。'); + } + + const targetIds = targetPurchases.map((purchase) => purchase.id); + + if (action === 'used' || action === 'unused') { + const nextUsedState = getLuckmailUsedPurchases(state); + targetIds.forEach((id) => { + const key = normalizeLuckmailPurchaseId(id); + if (!key) return; + if (action === 'used') { + nextUsedState[key] = true; + } else { + delete nextUsedState[key]; + } + }); + await setLuckmailUsedPurchasesState(nextUsedState); + await addLog(`LuckMail:已批量${action === 'used' ? '标记已用' : '标记未用'} ${targetIds.length} 个邮箱`, 'ok'); + } else if (action === 'preserve' || action === 'unpreserve') { + if (action === 'preserve') { + const preserveTag = await ensureLuckmailPreserveTag(client, state); + await client.user.batchSetPurchaseTag(targetIds, { tagId: preserveTag.id }); + } else { + await client.user.batchSetPurchaseTag(targetIds, { tagId: 0 }); + } + await addLog(`LuckMail:已批量${action === 'preserve' ? '保留' : '取消保留'} ${targetIds.length} 个邮箱`, 'ok'); + } else if (action === 'disable' || action === 'enable') { + await client.user.batchSetPurchaseDisabled(targetIds, action === 'disable' ? 1 : 0); + const currentPurchase = getCurrentLuckmailPurchase(await getState()); + if (action === 'disable' && currentPurchase?.id && targetIds.includes(currentPurchase.id)) { + await clearLuckmailRuntimeState({ clearEmail: isLuckmailProvider(await getState()) }); + } + await addLog(`LuckMail:已批量${action === 'disable' ? '禁用' : '启用'} ${targetIds.length} 个邮箱`, 'ok'); + } else { + throw new Error(`不支持的 LuckMail 批量操作:${action}`); + } + + return { + updatedIds: targetIds, + }; +} + +async function disableUsedLuckmailPurchases() { + const state = await ensureManualInteractionAllowed('禁用已用 LuckMail 邮箱'); + const usedPurchases = getLuckmailUsedPurchases(state); + const preserveTagInfo = getLuckmailPreserveTagInfo(state); + const client = createLuckmailClient(state); + const purchases = await listLuckmailPurchasesByProject(state, { + client, + projectCode: DEFAULT_LUCKMAIL_PROJECT_CODE, + }); + const targets = purchases.filter((purchase) => { + const purchaseId = normalizeLuckmailPurchaseId(purchase.id); + return Boolean(purchaseId && usedPurchases[purchaseId]) + && !isLuckmailPurchasePreserved(purchase, { + preserveTagId: preserveTagInfo.id, + preserveTagName: preserveTagInfo.name, + }) + && purchase.user_disabled !== 1; + }); + + if (!targets.length) { + return { disabledIds: [] }; + } + + const targetIds = targets.map((purchase) => purchase.id); + await client.user.batchSetPurchaseDisabled(targetIds, 1); + const currentPurchase = getCurrentLuckmailPurchase(await getState()); + if (currentPurchase?.id && targetIds.includes(currentPurchase.id)) { + await clearLuckmailRuntimeState({ clearEmail: isLuckmailProvider(await getState()) }); + } + await addLog(`LuckMail:已禁用 ${targetIds.length} 个本地已用邮箱`, 'ok'); + return { disabledIds: targetIds }; +} + +async function ensureLuckmailPurchaseForFlow(options = {}) { + const { allowReuse = true } = options; + const state = await getState(); + const existingPurchase = getCurrentLuckmailPurchase(state); + if (allowReuse && existingPurchase?.email_address && existingPurchase?.token) { + if (state.email !== existingPurchase.email_address) { + await setEmailState(existingPurchase.email_address); + } + return existingPurchase; + } + + const config = getLuckmailSessionConfig(state); + const client = createLuckmailClient(state); + if (allowReuse) { + const reusablePurchase = await findReusableLuckmailPurchaseForFlow(state, client); + if (reusablePurchase) { + return activateLuckmailPurchaseForFlow(state, client, reusablePurchase, { + initializeCursor: true, + logMessage: `LuckMail:已复用 openai 邮箱 ${reusablePurchase.email_address}`, + }); + } + } + + const result = await client.user.purchaseEmails(DEFAULT_LUCKMAIL_PROJECT_CODE, 1, { + emailType: config.emailType, + domain: config.domain || undefined, + }); + const purchases = normalizeLuckmailPurchases(result); + const purchase = purchases[0] || null; + if (!purchase?.email_address || !purchase?.token) { + throw new Error('LuckMail 购邮成功,但未返回可用邮箱或 token。'); + } + + return activateLuckmailPurchaseForFlow(state, client, purchase, { + initializeCursor: false, + logMessage: `LuckMail:已购买邮箱 ${purchase.email_address}(类型:${config.emailType},项目:${DEFAULT_LUCKMAIL_PROJECT_CODE})`, + }); +} + +async function resolveLuckmailVerificationMail(client, token, filters = {}, tokenCodeResult = null) { + const tokenCode = tokenCodeResult ? normalizeLuckmailTokenCode(tokenCodeResult) : null; + if (tokenCode?.mail) { + const tokenMail = tokenCode.verification_code && !tokenCode.mail.verification_code + ? { + ...tokenCode.mail, + verification_code: tokenCode.verification_code, + } + : tokenCode.mail; + const inlineMatch = pickLuckmailVerificationMail([tokenMail], filters); + if (inlineMatch) { + return inlineMatch; + } + } + + const mailList = await client.user.getTokenMails(token); + let match = pickLuckmailVerificationMail(mailList.mails, filters); + if (match?.mail?.message_id && !match.mail.verification_code) { + const detail = await client.user.getTokenMailDetail(token, match.mail.message_id); + match = pickLuckmailVerificationMail([detail], filters); + } + return match || null; +} + +async function pollLuckmailVerificationCode(step, state, pollPayload = {}) { + const purchase = getCurrentLuckmailPurchase(state); + if (!purchase?.token) { + throw new Error('LuckMail 当前没有可用 token,请先执行步骤 3 购买邮箱。'); + } + + const client = createLuckmailClient(state); + const maxAttempts = Math.max(1, Number(pollPayload.maxAttempts) || 5); + const intervalMs = Math.max(1000, Number(pollPayload.intervalMs) || 3000); + const filters = { + afterTimestamp: pollPayload.filterAfterTimestamp || 0, + senderFilters: pollPayload.senderFilters || [], + subjectFilters: pollPayload.subjectFilters || [], + excludeCodes: pollPayload.excludeCodes || [], + }; + + let lastError = null; + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + throwIfStopped(); + await addLog(`步骤 ${step}:正在通过 LuckMail 轮询验证码(${attempt}/${maxAttempts})...`, 'info'); + + try { + const tokenCode = await client.user.getTokenCode(purchase.token); + const cursor = normalizeLuckmailMailCursor((await getState()).currentLuckmailMailCursor); + if (tokenCode.verification_code && tokenCode.mail && !isLuckmailMailNewerThanCursor(tokenCode.mail, cursor)) { + throw new Error(`步骤 ${step}:LuckMail 返回的最新邮件仍是旧验证码。`); + } + + let match = null; + if (tokenCode.has_new_mail || tokenCode.verification_code) { + match = await resolveLuckmailVerificationMail(client, purchase.token, filters, tokenCode); + } + if (!match) { + match = await resolveLuckmailVerificationMail(client, purchase.token, filters, null); + } + + if (match?.mail) { + const cursor = normalizeLuckmailMailCursor((await getState()).currentLuckmailMailCursor); + if (!isLuckmailMailNewerThanCursor(match.mail, cursor)) { + throw new Error(`步骤 ${step}:LuckMail 命中的邮件不是新邮件。`); + } + + await setLuckmailMailCursorState(buildLuckmailMailCursor(match.mail)); + return { + ok: true, + code: match.code, + emailTimestamp: normalizeLuckmailTimestamp(match.mail.received_at) || Date.now(), + mailId: match.mail.message_id, + }; + } + + lastError = new Error(`步骤 ${step}:暂未在 LuckMail 邮箱中找到新的匹配验证码。`); + } catch (err) { + if (isStopError(err)) { + throw err; + } + lastError = err; + await addLog(`步骤 ${step}:LuckMail 轮询失败:${err.message}`, 'warn'); + } + + if (attempt < maxAttempts) { + await sleepWithStop(intervalMs); + } + } + + throw lastError || new Error(`步骤 ${step}:未在 LuckMail 邮箱中找到新的匹配验证码。`); +} + // ============================================================ // Tab Registry // ============================================================ @@ -2124,6 +3005,7 @@ function getSourceLabel(source) { 'inbucket-mail': 'Inbucket 邮箱', 'duck-mail': 'Duck 邮箱', 'hotmail-api': 'Hotmail(远程/本地)', + 'luckmail-api': 'LuckMail(API 购邮)', }; return labels[source] || source || '未知来源'; } @@ -2946,8 +3828,12 @@ async function handleMessage(message, sender) { case 'SAVE_SETTING': { const updates = buildPersistentSettingsPayload(message.payload || {}); + const sessionUpdates = buildLuckmailSessionSettingsPayload(message.payload || {}); await setPersistentSettings(updates); - await setState(updates); + await setState({ + ...updates, + ...sessionUpdates, + }); return { ok: true, state: await getState() }; } @@ -3017,6 +3903,41 @@ async function handleMessage(message, sender) { return { ok: true, ...result }; } + case 'LIST_LUCKMAIL_PURCHASES': { + const purchases = await listLuckmailPurchasesForManagement(); + return { ok: true, purchases }; + } + + case 'SELECT_LUCKMAIL_PURCHASE': { + const purchase = await selectLuckmailPurchase(message.payload?.purchaseId); + return { ok: true, purchase }; + } + + case 'SET_LUCKMAIL_PURCHASE_USED_STATE': { + const result = await setLuckmailPurchaseUsedState(message.payload?.purchaseId, Boolean(message.payload?.used)); + return { ok: true, ...result }; + } + + case 'SET_LUCKMAIL_PURCHASE_PRESERVED_STATE': { + const purchase = await setLuckmailPurchasePreservedState(message.payload?.purchaseId, Boolean(message.payload?.preserved)); + return { ok: true, purchase }; + } + + case 'SET_LUCKMAIL_PURCHASE_DISABLED_STATE': { + const purchase = await setLuckmailPurchaseDisabledState(message.payload?.purchaseId, Boolean(message.payload?.disabled)); + return { ok: true, purchase }; + } + + case 'BATCH_UPDATE_LUCKMAIL_PURCHASES': { + const result = await batchUpdateLuckmailPurchases(message.payload || {}); + return { ok: true, ...result }; + } + + case 'DISABLE_USED_LUCKMAIL_PURCHASES': { + const result = await disableUsedLuckmailPurchases(); + return { ok: true, ...result }; + } + // Side panel data updates case 'SET_EMAIL_STATE': { const state = await getState(); @@ -3139,6 +4060,15 @@ async function handleStepData(step, payload) { }); await addLog('当前 Hotmail 账号已自动标记为已用。', 'ok'); } + if (isLuckmailProvider(latestState)) { + const currentPurchase = getCurrentLuckmailPurchase(latestState); + if (currentPurchase?.id) { + await setLuckmailPurchaseUsedState(currentPurchase.id, true); + await addLog(`当前 LuckMail 邮箱 ${currentPurchase.email_address} 已在本地标记为已用。`, 'ok'); + } + await clearLuckmailRuntimeState({ clearEmail: true }); + await addLog('当前 LuckMail 邮箱运行态已清空,下轮将优先复用未用邮箱或重新购买邮箱。', 'ok'); + } const localhostPrefix = buildLocalhostCleanupPrefix(payload.localhostUrl); if (localhostPrefix) { await closeTabsByUrlPrefix(localhostPrefix); @@ -3564,6 +4494,12 @@ async function ensureAutoEmailReady(targetRun, totalRuns, attemptRuns) { return account.email; } + if (isLuckmailProvider(currentState)) { + const purchase = await ensureLuckmailPurchaseForFlow({ allowReuse: true }); + await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:LuckMail 邮箱已就绪:${purchase.email_address}(第 ${attemptRuns} 次尝试)===`, 'ok'); + return purchase.email_address; + } + if (isGeneratedAliasProvider(currentState.mailProvider)) { if (!currentState.emailPrefix) { throw new Error('2925 邮箱前缀未设置,请先在侧边栏填写。'); @@ -4690,6 +5626,9 @@ async function executeStep3(state) { preferredAccountId: state.currentHotmailAccountId || null, }); resolvedEmail = account.email; + } else if (isLuckmailProvider(state)) { + const purchase = await ensureLuckmailPurchaseForFlow({ allowReuse: true }); + resolvedEmail = purchase.email_address; } else if (isGeneratedAliasProvider(state.mailProvider)) { resolvedEmail = buildGeneratedAliasEmail(state); } @@ -4732,6 +5671,9 @@ function getMailConfig(state) { if (provider === HOTMAIL_PROVIDER) { return { provider: HOTMAIL_PROVIDER, label: 'Hotmail(远程/本地)' }; } + if (provider === LUCKMAIL_PROVIDER) { + return { provider: LUCKMAIL_PROVIDER, label: 'LuckMail(API 购邮)' }; + } if (provider === '163') { return { source: 'mail-163', url: 'https://mail.163.com/js6/main.jsp?df=mail163_letter#module=mbox.ListModule%7C%7B%22fid%22%3A1%2C%22order%22%3A%22date%22%2C%22desc%22%3Atrue%7D', label: '163 邮箱' }; } @@ -4896,6 +5838,12 @@ async function pollFreshVerificationCode(step, state, mail, pollOverrides = {}) ...pollOverrides, }); } + if (mail.provider === LUCKMAIL_PROVIDER) { + return pollLuckmailVerificationCode(step, state, { + ...getVerificationPollPayload(step, state), + ...pollOverrides, + }); + } const stateKey = getVerificationCodeStateKey(step); const rejectedCodes = new Set(); @@ -5111,7 +6059,7 @@ async function executeStep4(state) { } throwIfStopped(); - if (mail.provider === HOTMAIL_PROVIDER) { + if (mail.provider === HOTMAIL_PROVIDER || mail.provider === LUCKMAIL_PROVIDER) { await addLog(`步骤 4:正在通过 ${mail.label} 轮询验证码...`); } else { await addLog(`步骤 4:正在打开${mail.label}...`); @@ -5246,7 +6194,7 @@ async function runStep7Attempt(state) { } throwIfStopped(); - if (mail.provider === HOTMAIL_PROVIDER) { + if (mail.provider === HOTMAIL_PROVIDER || mail.provider === LUCKMAIL_PROVIDER) { await addLog(`步骤 7:正在通过 ${mail.label} 轮询验证码...`); } else { await addLog(`步骤 7:正在打开${mail.label}...`); diff --git a/luckmail-utils.js b/luckmail-utils.js new file mode 100644 index 0000000..e4b404d --- /dev/null +++ b/luckmail-utils.js @@ -0,0 +1,445 @@ +(function luckmailUtilsModule(root, factory) { + if (typeof module !== 'undefined' && module.exports) { + module.exports = factory(); + return; + } + + root.LuckMailUtils = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createLuckmailUtils() { + const DEFAULT_LUCKMAIL_BASE_URL = 'https://mails.luckyous.com'; + const DEFAULT_LUCKMAIL_EMAIL_TYPE = 'ms_graph'; + const DEFAULT_LUCKMAIL_PROJECT_CODE = 'openai'; + const DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME = '保留'; + const LUCKMAIL_EMAIL_TYPES = ['self_built', 'ms_imap', 'ms_graph', 'google_variant']; + + function firstNonEmptyString(values) { + for (const value of values) { + if (value === undefined || value === null) continue; + const normalized = String(value).trim(); + if (normalized) return normalized; + } + return ''; + } + + function normalizeText(value) { + return String(value || '') + .replace(/\s+/g, ' ') + .trim() + .toLowerCase(); + } + + function normalizeTimestamp(value) { + if (!value) return 0; + if (typeof value === 'number' && Number.isFinite(value)) { + return value > 0 ? value : 0; + } + + const rawValue = String(value || '').trim(); + const utcLikeMatch = rawValue.match( + /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?$/ + ); + if (utcLikeMatch && !/[zZ]|[+\-]\d{2}:?\d{2}$/.test(rawValue)) { + const [, year, month, day, hour, minute, second = '0'] = utcLikeMatch; + return Date.UTC( + Number(year), + Number(month) - 1, + Number(day), + Number(hour), + Number(minute), + Number(second), + 0 + ); + } + + const timestamp = Date.parse(rawValue); + return Number.isFinite(timestamp) ? timestamp : 0; + } + + function normalizeLuckmailBaseUrl(rawValue = '') { + const value = String(rawValue || '').trim(); + if (!value) return DEFAULT_LUCKMAIL_BASE_URL; + + try { + const parsed = new URL(value); + if (!['http:', 'https:'].includes(parsed.protocol)) { + return DEFAULT_LUCKMAIL_BASE_URL; + } + parsed.pathname = parsed.pathname.replace(/\/+$/, ''); + parsed.search = ''; + parsed.hash = ''; + return parsed.toString().replace(/\/$/, ''); + } catch { + return DEFAULT_LUCKMAIL_BASE_URL; + } + } + + function normalizeLuckmailEmailType(rawValue = '') { + const normalized = String(rawValue || '').trim().toLowerCase(); + return LUCKMAIL_EMAIL_TYPES.includes(normalized) + ? normalized + : DEFAULT_LUCKMAIL_EMAIL_TYPE; + } + + function normalizeLuckmailProjectName(rawValue = '') { + return normalizeText(rawValue); + } + + function extractLuckmailVerificationCode(text) { + const source = String(text || ''); + const matchCn = source.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/i); + if (matchCn) return matchCn[1]; + + const matchChatGPT = source.match(/your\s+chatgpt\s+code\s+is\s+(\d{6})/i); + if (matchChatGPT) return matchChatGPT[1]; + + const matchEn = source.match(/code(?:\s+is|[\s:])+(\d{6})/i); + if (matchEn) return matchEn[1]; + + const matchStandalone = source.match(/\b(\d{6})\b/); + return matchStandalone ? matchStandalone[1] : null; + } + + function normalizeLuckmailTag(item = {}) { + const safeItem = item && typeof item === 'object' ? item : {}; + return { + id: Number(safeItem.id) || 0, + name: firstNonEmptyString([safeItem.name, safeItem.tag_name]), + remark: firstNonEmptyString([safeItem.remark]), + limit_type: Number(safeItem.limit_type) || 0, + purchase_count: Number(safeItem.purchase_count) || 0, + created_at: firstNonEmptyString([safeItem.created_at]) || null, + }; + } + + function normalizeLuckmailTags(input) { + const list = Array.isArray(input?.list) + ? input.list + : (Array.isArray(input) ? input : []); + return list + .map((item) => normalizeLuckmailTag(item)) + .filter((item) => item.id > 0 || item.name); + } + + function normalizeLuckmailPurchase(item = {}) { + const safeItem = item && typeof item === 'object' ? item : {}; + const projectName = firstNonEmptyString([safeItem.project_name, safeItem.project]); + return { + id: Number(safeItem.id) || 0, + email_address: firstNonEmptyString([safeItem.email_address, safeItem.address]), + token: firstNonEmptyString([safeItem.token]), + project_name: projectName, + project_code: normalizeLuckmailProjectName(projectName), + price: firstNonEmptyString([safeItem.price]) || '0.0000', + status: Number(safeItem.status) || 0, + tag_id: Number(safeItem.tag_id) || 0, + tag_name: firstNonEmptyString([safeItem.tag_name]), + user_disabled: Number(safeItem.user_disabled) || 0, + warranty_hours: Number(safeItem.warranty_hours) || 0, + warranty_until: firstNonEmptyString([safeItem.warranty_until]) || null, + created_at: firstNonEmptyString([safeItem.created_at]) || null, + }; + } + + function normalizeLuckmailPurchases(result) { + const list = Array.isArray(result?.purchases) + ? result.purchases + : (Array.isArray(result) ? result : []); + return list.map((item) => normalizeLuckmailPurchase(item)); + } + + function normalizeLuckmailPurchaseListPage(result = {}) { + const safeResult = result && typeof result === 'object' ? result : {}; + const list = Array.isArray(safeResult.list) + ? safeResult.list + : (Array.isArray(safeResult.purchases) ? safeResult.purchases : []); + const total = Number(safeResult.total); + return { + list: list.map((item) => normalizeLuckmailPurchase(item)), + total: Number.isFinite(total) && total >= 0 ? total : 0, + page: Math.max(1, Number(safeResult.page) || 1), + page_size: Math.max(1, Number(safeResult.page_size || safeResult.pageSize) || list.length || 1), + }; + } + + function normalizeLuckmailPurchaseId(value) { + const numeric = Number(value); + if (!Number.isFinite(numeric) || numeric <= 0) { + return ''; + } + return String(Math.floor(numeric)); + } + + function normalizeLuckmailUsedPurchases(rawValue = {}) { + if (!rawValue || typeof rawValue !== 'object' || Array.isArray(rawValue)) { + return {}; + } + + return Object.entries(rawValue).reduce((result, [key, value]) => { + const normalizedKey = normalizeLuckmailPurchaseId(key); + if (!normalizedKey) { + return result; + } + result[normalizedKey] = Boolean(value); + return result; + }, {}); + } + + function isLuckmailPurchaseForProject(purchase, projectCode = DEFAULT_LUCKMAIL_PROJECT_CODE) { + const normalizedPurchase = normalizeLuckmailPurchase(purchase); + return normalizeLuckmailProjectName(normalizedPurchase.project_name) === normalizeLuckmailProjectName(projectCode); + } + + function normalizeLuckmailTokenMail(mail = {}) { + const safeMail = mail && typeof mail === 'object' ? mail : {}; + const subject = firstNonEmptyString([safeMail.subject, safeMail.title]); + const body = firstNonEmptyString([safeMail.body, safeMail.body_text, safeMail.text]); + const htmlBody = firstNonEmptyString([safeMail.html_body, safeMail.body_html, safeMail.html]); + const from = firstNonEmptyString([safeMail.from, safeMail.sender]); + const verificationCode = firstNonEmptyString([safeMail.verification_code]) + || extractLuckmailVerificationCode([subject, body, htmlBody, from].filter(Boolean).join(' ')); + + return { + message_id: firstNonEmptyString([safeMail.message_id, safeMail.id]), + from, + subject, + body, + html_body: htmlBody, + received_at: firstNonEmptyString([safeMail.received_at, safeMail.receivedAt, safeMail.created_at]), + verification_code: verificationCode || '', + }; + } + + function normalizeLuckmailTokenMails(input) { + const list = Array.isArray(input?.mails) + ? input.mails + : (Array.isArray(input) ? input : []); + return list.map((mail) => normalizeLuckmailTokenMail(mail)); + } + + function normalizeLuckmailTokenCode(result = {}) { + return { + email_address: firstNonEmptyString([result.email_address, result.address]), + project: firstNonEmptyString([result.project]), + has_new_mail: Boolean(result.has_new_mail), + verification_code: firstNonEmptyString([result.verification_code]) || null, + mail: result.mail ? normalizeLuckmailTokenMail(result.mail) : null, + }; + } + + function normalizeLuckmailMailCursor(cursor = {}) { + const safeCursor = cursor && typeof cursor === 'object' ? cursor : {}; + return { + messageId: firstNonEmptyString([safeCursor.messageId, safeCursor.message_id]), + receivedAt: firstNonEmptyString([safeCursor.receivedAt, safeCursor.received_at]), + }; + } + + function buildLuckmailMailCursor(mail = {}) { + const normalizedMail = normalizeLuckmailTokenMail(mail); + return normalizeLuckmailMailCursor({ + messageId: normalizedMail.message_id, + receivedAt: normalizedMail.received_at, + }); + } + + function buildLuckmailBaselineCursor(mails) { + const latestMail = normalizeLuckmailTokenMails(mails) + .sort((left, right) => { + const leftTimestamp = normalizeTimestamp(left.received_at); + const rightTimestamp = normalizeTimestamp(right.received_at); + if (leftTimestamp !== rightTimestamp) { + return rightTimestamp - leftTimestamp; + } + return String(right.message_id || '').localeCompare(String(left.message_id || '')); + })[0] || null; + + return latestMail ? buildLuckmailMailCursor(latestMail) : null; + } + + function isLuckmailMailNewerThanCursor(mail = {}, cursor = {}) { + const normalizedMail = normalizeLuckmailTokenMail(mail); + const normalizedCursor = normalizeLuckmailMailCursor(cursor); + + if (!normalizedCursor.messageId && !normalizedCursor.receivedAt) { + return true; + } + + if (normalizedMail.message_id && normalizedCursor.messageId && normalizedMail.message_id === normalizedCursor.messageId) { + return false; + } + + const mailTimestamp = normalizeTimestamp(normalizedMail.received_at); + const cursorTimestamp = normalizeTimestamp(normalizedCursor.receivedAt); + + if (mailTimestamp && cursorTimestamp) { + if (mailTimestamp > cursorTimestamp) return true; + if (mailTimestamp < cursorTimestamp) return false; + return Boolean( + normalizedMail.message_id + && normalizedCursor.messageId + && normalizedMail.message_id !== normalizedCursor.messageId + ); + } + + if (normalizedMail.message_id && normalizedCursor.messageId) { + return normalizedMail.message_id !== normalizedCursor.messageId; + } + + return !cursorTimestamp || Boolean(mailTimestamp && mailTimestamp > cursorTimestamp); + } + + function isLuckmailPurchaseExpired(purchase, now = Date.now()) { + const normalizedPurchase = normalizeLuckmailPurchase(purchase); + const expiresAt = normalizeTimestamp(normalizedPurchase.warranty_until); + return Boolean(expiresAt && expiresAt <= Number(now || 0)); + } + + function isLuckmailPurchaseDisabled(purchase) { + return Number(normalizeLuckmailPurchase(purchase).user_disabled) === 1; + } + + function isLuckmailPurchasePreserved(purchase, options = {}) { + const normalizedPurchase = normalizeLuckmailPurchase(purchase); + const expectedTagId = Number(options.preserveTagId) || 0; + const expectedTagName = normalizeText(options.preserveTagName || DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME); + + if (expectedTagId > 0 && normalizedPurchase.tag_id === expectedTagId) { + return true; + } + + return Boolean(normalizedPurchase.tag_name && normalizeText(normalizedPurchase.tag_name) === expectedTagName); + } + + function isLuckmailPurchaseReusable(purchase, options = {}) { + const normalizedPurchase = normalizeLuckmailPurchase(purchase); + const usedPurchases = normalizeLuckmailUsedPurchases(options.usedPurchases); + const purchaseId = normalizeLuckmailPurchaseId(normalizedPurchase.id); + + if (!isLuckmailPurchaseForProject(normalizedPurchase, options.projectCode || DEFAULT_LUCKMAIL_PROJECT_CODE)) { + return false; + } + if (!normalizedPurchase.email_address || !normalizedPurchase.token) { + return false; + } + if (isLuckmailPurchaseDisabled(normalizedPurchase)) { + return false; + } + if (purchaseId && usedPurchases[purchaseId]) { + return false; + } + if (isLuckmailPurchasePreserved(normalizedPurchase, options)) { + return false; + } + if (isLuckmailPurchaseExpired(normalizedPurchase, options.now || Date.now())) { + return false; + } + return true; + } + + function filterReusableLuckmailPurchases(purchases, options = {}) { + const list = Array.isArray(purchases) + ? purchases + : normalizeLuckmailPurchaseListPage(purchases).list; + return list + .map((purchase) => normalizeLuckmailPurchase(purchase)) + .filter((purchase) => isLuckmailPurchaseReusable(purchase, options)); + } + + function pickReusableLuckmailPurchase(purchases, options = {}) { + return filterReusableLuckmailPurchases(purchases, options)[0] || null; + } + + function mailMatchesLuckmailFilters(mail, filters = {}) { + const normalizedMail = normalizeLuckmailTokenMail(mail); + const afterTimestamp = normalizeTimestamp(filters.afterTimestamp); + const receivedAt = normalizeTimestamp(normalizedMail.received_at); + if (afterTimestamp && receivedAt && receivedAt < afterTimestamp) { + return null; + } + + const senderFilters = (filters.senderFilters || []).map(normalizeText).filter(Boolean); + const subjectFilters = (filters.subjectFilters || []).map(normalizeText).filter(Boolean); + const excludedCodes = new Set((filters.excludeCodes || []).filter(Boolean)); + const combinedText = [ + normalizedMail.subject, + normalizedMail.from, + normalizedMail.body, + normalizedMail.html_body, + ].filter(Boolean).join(' '); + const combinedTextNormalized = normalizeText(combinedText); + const senderNormalized = normalizeText(normalizedMail.from); + const subjectNormalized = normalizeText(normalizedMail.subject); + const code = normalizedMail.verification_code || extractLuckmailVerificationCode(combinedText); + + if (!code || excludedCodes.has(code)) { + return null; + } + + const senderMatch = senderFilters.length === 0 + ? true + : senderFilters.some((item) => senderNormalized.includes(item) || combinedTextNormalized.includes(item)); + const subjectMatch = subjectFilters.length === 0 + ? true + : subjectFilters.some((item) => subjectNormalized.includes(item) || combinedTextNormalized.includes(item)); + + if (!senderMatch && !subjectMatch) { + return null; + } + + return { + code, + mail: normalizedMail, + receivedAt, + }; + } + + function pickLuckmailVerificationMail(mails, filters = {}) { + const matches = normalizeLuckmailTokenMails(mails) + .map((mail) => mailMatchesLuckmailFilters(mail, filters)) + .filter(Boolean) + .sort((left, right) => { + if (left.receivedAt !== right.receivedAt) { + return right.receivedAt - left.receivedAt; + } + return String(right.mail.message_id || '').localeCompare(String(left.mail.message_id || '')); + }); + + return matches[0] || null; + } + + return { + DEFAULT_LUCKMAIL_BASE_URL, + DEFAULT_LUCKMAIL_EMAIL_TYPE, + DEFAULT_LUCKMAIL_PROJECT_CODE, + DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME, + LUCKMAIL_EMAIL_TYPES, + buildLuckmailBaselineCursor, + buildLuckmailMailCursor, + extractLuckmailVerificationCode, + filterReusableLuckmailPurchases, + isLuckmailMailNewerThanCursor, + isLuckmailPurchaseDisabled, + isLuckmailPurchaseExpired, + isLuckmailPurchaseForProject, + isLuckmailPurchasePreserved, + isLuckmailPurchaseReusable, + normalizeLuckmailBaseUrl, + normalizeLuckmailEmailType, + normalizeLuckmailMailCursor, + normalizeLuckmailProjectName, + normalizeLuckmailPurchase, + normalizeLuckmailPurchaseId, + normalizeLuckmailPurchaseListPage, + normalizeLuckmailPurchases, + normalizeLuckmailTag, + normalizeLuckmailTags, + normalizeLuckmailTokenCode, + normalizeLuckmailTokenMail, + normalizeLuckmailTokenMails, + normalizeLuckmailUsedPurchases, + normalizeText, + normalizeTimestamp, + pickLuckmailVerificationMail, + pickReusableLuckmailPurchase, + }; +}); diff --git a/sidepanel/sidepanel.css b/sidepanel/sidepanel.css index e6bbe94..d11eb72 100644 --- a/sidepanel/sidepanel.css +++ b/sidepanel/sidepanel.css @@ -698,6 +698,164 @@ header { margin-top: 10px; } +.luckmail-card { + margin-top: 10px; +} + +.luckmail-manager-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; +} + +.luckmail-summary { + color: var(--text-secondary); + font-size: 12px; + line-height: 1.6; +} + +.luckmail-toolbar { + display: flex; + gap: 8px; + align-items: center; +} + +.luckmail-search { + flex: 1; +} + +.luckmail-filter { + flex: 0 0 130px; +} + +.luckmail-bulkbar { + display: flex; + gap: 8px; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; +} + +.luckmail-select-all { + flex: 0 0 auto; +} + +.luckmail-bulk-actions { + display: flex; + gap: 6px; + flex-wrap: wrap; +} + +.luckmail-list { + display: flex; + flex-direction: column; + gap: 8px; + max-height: 360px; + overflow: auto; + padding-right: 4px; +} + +.luckmail-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; +} + +.luckmail-item.is-current { + border-color: var(--blue); + box-shadow: 0 0 0 1px var(--blue-glow); +} + +.luckmail-item-check { + margin-top: 4px; +} + +.luckmail-item-main { + min-width: 0; + display: flex; + flex-direction: column; + gap: 6px; +} + +.luckmail-item-email-row { + display: flex; + align-items: center; + gap: 6px; +} + +.luckmail-item-email { + font-weight: 600; + color: var(--text-primary); + word-break: break-all; +} + +.luckmail-item-meta, +.luckmail-item-details { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.luckmail-item-details { + color: var(--text-secondary); + font-size: 12px; +} + +.luckmail-item-actions { + display: flex; + gap: 6px; + flex-wrap: wrap; + justify-content: flex-end; +} + +.luckmail-tag { + display: inline-flex; + align-items: center; + padding: 2px 8px; + border-radius: 999px; + font-size: 11px; + background: var(--bg-surface); + color: var(--text-secondary); +} + +.luckmail-tag.used { + background: var(--orange-soft); + color: var(--orange); +} + +.luckmail-tag.active { + background: var(--green-soft); + color: var(--green); +} + +.luckmail-tag.current { + background: var(--blue-soft); + color: var(--blue); +} + +.luckmail-tag.disabled { + background: var(--red-soft); + color: var(--red); +} + +.luckmail-empty { + color: var(--text-muted); + font-size: 12px; + border: 1px dashed var(--border); + border-radius: var(--radius-sm); + padding: 12px; + text-align: center; +} + .hotmail-actions-row .data-label { visibility: hidden; } diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 944b7e7..b57ec4d 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -154,6 +154,7 @@ + +
+ Base URL + +
+
+ 邮箱类型 + +
+
+ 域名 + +
+
+ 项目 + openai +
+
+
+
+ + 仅显示项目为 openai 的邮箱 +
+
+ + +
+
+
加载已购邮箱后可在这里管理 openai 项目的 LuckMail 邮箱。
+
+ + +
+
+ +
+ + + + + + +
+
+
+
+
就绪 @@ -432,6 +504,7 @@
+ diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index 623f020..20402e3 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -69,6 +69,7 @@ const btnMailLogin = document.getElementById('btn-mail-login'); const rowEmailGenerator = document.getElementById('row-email-generator'); const selectEmailGenerator = document.getElementById('select-email-generator'); const hotmailSection = document.getElementById('hotmail-section'); +const luckmailSection = document.getElementById('luckmail-section'); 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'); @@ -88,6 +89,24 @@ const btnDeleteAllHotmailAccounts = document.getElementById('btn-delete-all-hotm const btnToggleHotmailList = document.getElementById('btn-toggle-hotmail-list'); const hotmailListShell = document.getElementById('hotmail-list-shell'); const hotmailAccountsList = document.getElementById('hotmail-accounts-list'); +const inputLuckmailApiKey = document.getElementById('input-luckmail-api-key'); +const inputLuckmailBaseUrl = document.getElementById('input-luckmail-base-url'); +const selectLuckmailEmailType = document.getElementById('select-luckmail-email-type'); +const inputLuckmailDomain = document.getElementById('input-luckmail-domain'); +const btnLuckmailRefresh = document.getElementById('btn-luckmail-refresh'); +const btnLuckmailDisableUsed = document.getElementById('btn-luckmail-disable-used'); +const luckmailSummary = document.getElementById('luckmail-summary'); +const inputLuckmailSearch = document.getElementById('input-luckmail-search'); +const selectLuckmailFilter = document.getElementById('select-luckmail-filter'); +const checkboxLuckmailSelectAll = document.getElementById('checkbox-luckmail-select-all'); +const luckmailSelectionSummary = document.getElementById('luckmail-selection-summary'); +const btnLuckmailBulkUsed = document.getElementById('btn-luckmail-bulk-used'); +const btnLuckmailBulkUnused = document.getElementById('btn-luckmail-bulk-unused'); +const btnLuckmailBulkPreserve = document.getElementById('btn-luckmail-bulk-preserve'); +const btnLuckmailBulkUnpreserve = document.getElementById('btn-luckmail-bulk-unpreserve'); +const btnLuckmailBulkDisable = document.getElementById('btn-luckmail-bulk-disable'); +const btnLuckmailBulkEnable = document.getElementById('btn-luckmail-bulk-enable'); +const luckmailList = document.getElementById('luckmail-list'); const rowEmailPrefix = document.getElementById('row-email-prefix'); const labelEmailPrefix = document.getElementById('label-email-prefix'); const inputEmailPrefix = document.getElementById('input-email-prefix'); @@ -145,6 +164,9 @@ const AUTO_RUN_FALLBACK_RISK_WARNING_MIN_RUNS = 15; const AUTO_RUN_FALLBACK_RISK_RECOMMENDED_THREAD_INTERVAL_MINUTES = 5; const HOTMAIL_SERVICE_MODE_REMOTE = 'remote'; const HOTMAIL_SERVICE_MODE_LOCAL = 'local'; +const LUCKMAIL_PROVIDER = 'luckmail-api'; +const DEFAULT_LUCKMAIL_BASE_URL = 'https://mails.luckyous.com'; +const DEFAULT_LUCKMAIL_EMAIL_TYPE = 'ms_graph'; const DISPLAY_TIMEZONE = 'Asia/Shanghai'; let latestState = null; @@ -182,8 +204,20 @@ const upsertHotmailAccountInList = window.HotmailUtils?.upsertHotmailAccountInLi const filterHotmailAccountsByUsage = window.HotmailUtils?.filterHotmailAccountsByUsage; const getHotmailBulkActionLabel = window.HotmailUtils?.getHotmailBulkActionLabel; const getHotmailListToggleLabel = window.HotmailUtils?.getHotmailListToggleLabel; +const normalizeLuckmailTimestampValue = window.LuckMailUtils?.normalizeTimestamp + || ((value) => { + const timestamp = Date.parse(String(value || '')); + return Number.isFinite(timestamp) ? timestamp : 0; + }); const HOTMAIL_LIST_EXPANDED_STORAGE_KEY = 'multipage-hotmail-list-expanded'; const sidepanelUpdateService = window.SidepanelUpdateService; +const DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME = window.LuckMailUtils?.DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME || '保留'; + +let lastRenderedLuckmailPurchases = []; +let luckmailSelectedPurchaseIds = new Set(); +let luckmailSearchTerm = ''; +let luckmailFilterMode = 'all'; +let luckmailRefreshQueued = false; btnAutoCancelSchedule?.remove(); const MAIL_PROVIDER_LOGIN_CONFIGS = { @@ -933,6 +967,10 @@ function collectSettingsPayload() { hotmailServiceMode: getSelectedHotmailServiceMode(), hotmailRemoteBaseUrl: inputHotmailRemoteBaseUrl.value.trim(), hotmailLocalBaseUrl: inputHotmailLocalBaseUrl.value.trim(), + luckmailApiKey: inputLuckmailApiKey.value, + luckmailBaseUrl: normalizeLuckmailBaseUrl(inputLuckmailBaseUrl.value), + luckmailEmailType: normalizeLuckmailEmailType(selectLuckmailEmailType.value), + luckmailDomain: inputLuckmailDomain.value.trim(), cloudflareDomain: selectedCloudflareDomain, cloudflareDomains: domains, autoRunSkipFailures: inputAutoSkipFailures.checked, @@ -1206,7 +1244,7 @@ function applySettingsState(state) { inputSub2ApiPassword.value = state?.sub2apiPassword || ''; inputSub2ApiGroup.value = state?.sub2apiGroupName || ''; const restoredMailProvider = isCustomMailProvider(state?.mailProvider) - || ['hotmail-api', '163', '163-vip', 'qq', 'inbucket', '2925'].includes(String(state?.mailProvider || '').trim()) + || ['hotmail-api', 'luckmail-api', '163', '163-vip', 'qq', 'inbucket', '2925'].includes(String(state?.mailProvider || '').trim()) ? String(state?.mailProvider || '163').trim() : (String(state?.emailGenerator || '').trim().toLowerCase() === 'custom' || String(state?.emailGenerator || '').trim().toLowerCase() === 'manual' @@ -1220,6 +1258,10 @@ function applySettingsState(state) { setHotmailServiceMode(state?.hotmailServiceMode); inputHotmailRemoteBaseUrl.value = state?.hotmailRemoteBaseUrl || ''; inputHotmailLocalBaseUrl.value = state?.hotmailLocalBaseUrl || ''; + inputLuckmailApiKey.value = state?.luckmailApiKey || ''; + inputLuckmailBaseUrl.value = normalizeLuckmailBaseUrl(state?.luckmailBaseUrl); + selectLuckmailEmailType.value = normalizeLuckmailEmailType(state?.luckmailEmailType); + inputLuckmailDomain.value = state?.luckmailDomain || ''; renderCloudflareDomainOptions(state?.cloudflareDomain || ''); setCloudflareDomainEditMode(false, { clearInput: true }); inputAutoSkipFailures.checked = Boolean(state?.autoRunSkipFailures); @@ -1237,6 +1279,9 @@ function applySettingsState(state) { updateFallbackThreadIntervalInputState(); updatePanelModeUI(); updateMailProviderUI(); + if (isLuckmailProvider(state?.mailProvider)) { + queueLuckmailPurchaseRefresh(); + } updateButtonStates(); } @@ -1486,6 +1531,37 @@ function isCustomMailProvider(provider = selectMailProvider.value) { return String(provider || '').trim().toLowerCase() === 'custom'; } +function isLuckmailProvider(provider = selectMailProvider.value) { + return String(provider || '').trim().toLowerCase() === LUCKMAIL_PROVIDER; +} + +function normalizeLuckmailBaseUrl(value = '') { + const trimmed = String(value || '').trim(); + if (!trimmed) { + return DEFAULT_LUCKMAIL_BASE_URL; + } + + try { + const parsed = new URL(trimmed); + if (!['http:', 'https:'].includes(parsed.protocol)) { + return DEFAULT_LUCKMAIL_BASE_URL; + } + parsed.pathname = parsed.pathname.replace(/\/+$/, ''); + parsed.search = ''; + parsed.hash = ''; + return parsed.toString().replace(/\/$/, ''); + } catch { + return DEFAULT_LUCKMAIL_BASE_URL; + } +} + +function normalizeLuckmailEmailType(value = '') { + const normalized = String(value || '').trim().toLowerCase(); + return ['self_built', 'ms_imap', 'ms_graph', 'google_variant'].includes(normalized) + ? normalized + : DEFAULT_LUCKMAIL_EMAIL_TYPE; +} + function getSelectedEmailGenerator() { const generator = String(selectEmailGenerator.value || '').trim().toLowerCase(); if (generator === 'cloudflare') { @@ -1546,6 +1622,417 @@ function getCurrentHotmailEmail(state = latestState) { return String(getCurrentHotmailAccount(state)?.email || '').trim(); } +function getCurrentLuckmailPurchase(state = latestState) { + return state?.currentLuckmailPurchase || null; +} + +function getCurrentLuckmailEmail(state = latestState) { + return String(getCurrentLuckmailPurchase(state)?.email_address || '').trim(); +} + +function getLuckmailUsedPurchases(state = latestState) { + const rawValue = state?.luckmailUsedPurchases; + if (!rawValue || typeof rawValue !== 'object' || Array.isArray(rawValue)) { + return {}; + } + + return Object.entries(rawValue).reduce((result, [key, value]) => { + const numeric = Number(key); + if (!Number.isFinite(numeric) || numeric <= 0) { + return result; + } + result[String(Math.floor(numeric))] = Boolean(value); + return result; + }, {}); +} + +function normalizeLuckmailProjectName(value = '') { + return String(value || '').trim().toLowerCase(); +} + +function getLuckmailPreserveTagName(state = latestState) { + return String(state?.luckmailPreserveTagName || '').trim() || DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME; +} + +function formatLuckmailDateTime(value) { + const timestamp = normalizeLuckmailTimestampValue(value); + if (!timestamp) { + return String(value || '').trim() || '未知'; + } + return new Date(timestamp).toLocaleString('zh-CN', { + hour12: false, + timeZone: DISPLAY_TIMEZONE, + }); +} + +function normalizeLuckmailSearchText(value) { + return String(value || '').trim().toLowerCase(); +} + +function getFilteredLuckmailPurchases(purchases = lastRenderedLuckmailPurchases) { + const searchTerm = normalizeLuckmailSearchText(luckmailSearchTerm); + return (Array.isArray(purchases) ? purchases : []).filter((purchase) => { + const matchesFilter = (() => { + switch (luckmailFilterMode) { + case 'reusable': return Boolean(purchase.reusable); + case 'used': return Boolean(purchase.used); + case 'unused': return !purchase.used; + case 'preserved': return Boolean(purchase.preserved); + case 'disabled': return Boolean(purchase.disabled); + default: return true; + } + })(); + + if (!matchesFilter) return false; + if (!searchTerm) return true; + + const haystack = [ + purchase.email_address, + purchase.project_name, + purchase.tag_name, + purchase.used ? '已用 used' : '未用 unused', + purchase.preserved ? '保留 preserved' : '', + purchase.disabled ? '已禁用 disabled' : '', + purchase.reusable ? '可复用 reusable' : '', + ].join(' ').toLowerCase(); + + return haystack.includes(searchTerm); + }); +} + +function pruneLuckmailSelection(purchases = lastRenderedLuckmailPurchases) { + const existingIds = new Set((Array.isArray(purchases) ? purchases : []).map((purchase) => String(purchase.id))); + luckmailSelectedPurchaseIds = new Set([...luckmailSelectedPurchaseIds].filter((id) => existingIds.has(id))); +} + +function updateLuckmailBulkUI(visiblePurchases = getFilteredLuckmailPurchases()) { + if (!checkboxLuckmailSelectAll || !luckmailSelectionSummary) { + return; + } + + const visibleIds = visiblePurchases.map((purchase) => String(purchase.id)); + const selectedVisibleCount = visibleIds.filter((id) => luckmailSelectedPurchaseIds.has(id)).length; + const hasVisible = visibleIds.length > 0; + const hasSelection = luckmailSelectedPurchaseIds.size > 0; + + checkboxLuckmailSelectAll.checked = hasVisible && selectedVisibleCount === visibleIds.length; + checkboxLuckmailSelectAll.indeterminate = selectedVisibleCount > 0 && selectedVisibleCount < visibleIds.length; + checkboxLuckmailSelectAll.disabled = !hasVisible; + luckmailSelectionSummary.textContent = `已选 ${luckmailSelectedPurchaseIds.size} 个(当前显示 ${visibleIds.length} 个)`; + + if (btnLuckmailBulkUsed) btnLuckmailBulkUsed.disabled = !hasSelection; + if (btnLuckmailBulkUnused) btnLuckmailBulkUnused.disabled = !hasSelection; + if (btnLuckmailBulkPreserve) btnLuckmailBulkPreserve.disabled = !hasSelection; + if (btnLuckmailBulkUnpreserve) btnLuckmailBulkUnpreserve.disabled = !hasSelection; + if (btnLuckmailBulkDisable) btnLuckmailBulkDisable.disabled = !hasSelection; + if (btnLuckmailBulkEnable) btnLuckmailBulkEnable.disabled = !hasSelection; +} + +function setLuckmailLoadingState(loading, summary = '') { + if (btnLuckmailRefresh) btnLuckmailRefresh.disabled = loading; + if (btnLuckmailDisableUsed) btnLuckmailDisableUsed.disabled = loading; + if (inputLuckmailSearch) inputLuckmailSearch.disabled = loading; + if (selectLuckmailFilter) selectLuckmailFilter.disabled = loading; + if (checkboxLuckmailSelectAll) checkboxLuckmailSelectAll.disabled = loading || getFilteredLuckmailPurchases().length === 0; + if (btnLuckmailBulkUsed) btnLuckmailBulkUsed.disabled = loading || luckmailSelectedPurchaseIds.size === 0; + if (btnLuckmailBulkUnused) btnLuckmailBulkUnused.disabled = loading || luckmailSelectedPurchaseIds.size === 0; + if (btnLuckmailBulkPreserve) btnLuckmailBulkPreserve.disabled = loading || luckmailSelectedPurchaseIds.size === 0; + if (btnLuckmailBulkUnpreserve) btnLuckmailBulkUnpreserve.disabled = loading || luckmailSelectedPurchaseIds.size === 0; + if (btnLuckmailBulkDisable) btnLuckmailBulkDisable.disabled = loading || luckmailSelectedPurchaseIds.size === 0; + if (btnLuckmailBulkEnable) btnLuckmailBulkEnable.disabled = loading || luckmailSelectedPurchaseIds.size === 0; + if (summary && luckmailSummary) { + luckmailSummary.textContent = summary; + } +} + +function renderLuckmailPurchases(purchases = []) { + if (!luckmailList || !luckmailSummary) return; + + lastRenderedLuckmailPurchases = Array.isArray(purchases) ? purchases : []; + pruneLuckmailSelection(lastRenderedLuckmailPurchases); + luckmailList.innerHTML = ''; + + if (!lastRenderedLuckmailPurchases.length) { + luckmailSelectedPurchaseIds.clear(); + luckmailList.innerHTML = '
未找到 openai 项目的 LuckMail 邮箱。
'; + luckmailSummary.textContent = '加载已购邮箱后可在这里管理 openai 项目的 LuckMail 邮箱。'; + if (btnLuckmailDisableUsed) btnLuckmailDisableUsed.disabled = true; + updateLuckmailBulkUI([]); + return; + } + + const usedCount = lastRenderedLuckmailPurchases.filter((purchase) => purchase.used).length; + const reusableCount = lastRenderedLuckmailPurchases.filter((purchase) => purchase.reusable).length; + const disableUsedCount = lastRenderedLuckmailPurchases.filter((purchase) => purchase.used && !purchase.preserved && !purchase.disabled).length; + luckmailSummary.textContent = `已加载 ${lastRenderedLuckmailPurchases.length} 个 openai 邮箱,其中 ${reusableCount} 个可复用,${usedCount} 个已本地标记为已用。`; + if (btnLuckmailDisableUsed) { + btnLuckmailDisableUsed.textContent = `禁用已用${disableUsedCount > 0 ? `(${disableUsedCount})` : ''}`; + btnLuckmailDisableUsed.disabled = disableUsedCount === 0; + } + + const visiblePurchases = getFilteredLuckmailPurchases(lastRenderedLuckmailPurchases); + if (!visiblePurchases.length) { + luckmailList.innerHTML = '
没有匹配当前筛选条件的 LuckMail 邮箱。
'; + updateLuckmailBulkUI([]); + return; + } + + for (const purchase of visiblePurchases) { + const purchaseId = String(purchase.id); + const item = document.createElement('div'); + item.className = `luckmail-item${purchase.current ? ' is-current' : ''}`; + item.innerHTML = ` + +
+ +
+ ${escapeHtml(normalizeLuckmailProjectName(purchase.project_name) || 'openai')} + ${purchase.reusable ? '可复用' : ''} + ${purchase.current ? '当前' : ''} + ${purchase.used ? '已用' : ''} + ${purchase.preserved ? '保留' : ''} + ${purchase.disabled ? '已禁用' : ''} + ${purchase.tag_name && normalizeLuckmailSearchText(purchase.tag_name) !== normalizeLuckmailSearchText(getLuckmailPreserveTagName()) + ? `${escapeHtml(purchase.tag_name)}` + : ''} +
+
+ ID:${escapeHtml(String(purchase.id || ''))} + 保修至:${escapeHtml(formatLuckmailDateTime(purchase.warranty_until))} +
+
+
+ + + + +
+ `; + + item.querySelector('[data-action="select"]').addEventListener('change', (event) => { + if (event.target.checked) { + luckmailSelectedPurchaseIds.add(purchaseId); + } else { + luckmailSelectedPurchaseIds.delete(purchaseId); + } + updateLuckmailBulkUI(visiblePurchases); + }); + item.querySelector('[data-action="copy-email"]').addEventListener('click', async () => { + await copyTextToClipboard(purchase.email_address || ''); + showToast('邮箱已复制', 'success', 1600); + }); + item.querySelector('[data-action="use"]').addEventListener('click', async () => { + await selectSingleLuckmailPurchase(purchase); + }); + item.querySelector('[data-action="toggle-used"]').addEventListener('click', async () => { + await setSingleLuckmailPurchaseUsedState(purchase, !purchase.used); + }); + item.querySelector('[data-action="toggle-preserved"]').addEventListener('click', async () => { + await setSingleLuckmailPurchasePreservedState(purchase, !purchase.preserved); + }); + item.querySelector('[data-action="toggle-disabled"]').addEventListener('click', async () => { + await setSingleLuckmailPurchaseDisabledState(purchase, !purchase.disabled); + }); + luckmailList.appendChild(item); + } + + updateLuckmailBulkUI(visiblePurchases); +} + +async function refreshLuckmailPurchases(options = {}) { + const { silent = false } = options; + if (!luckmailSection || luckmailSection.style.display === 'none') { + return; + } + + if (!silent) setLuckmailLoadingState(true, '正在加载 LuckMail openai 邮箱...'); + try { + const response = await chrome.runtime.sendMessage({ + type: 'LIST_LUCKMAIL_PURCHASES', + source: 'sidepanel', + payload: {}, + }); + if (response?.error) throw new Error(response.error); + renderLuckmailPurchases(response?.purchases || []); + } catch (err) { + luckmailSelectedPurchaseIds.clear(); + if (luckmailList) { + luckmailList.innerHTML = '
无法加载 LuckMail 邮箱列表。
'; + } + if (luckmailSummary) { + luckmailSummary.textContent = err.message; + } + updateLuckmailBulkUI([]); + if (!silent) { + showToast(`LuckMail 邮箱列表加载失败:${err.message}`, 'error'); + } + } finally { + setLuckmailLoadingState(false); + } +} + +function queueLuckmailPurchaseRefresh() { + if (luckmailRefreshQueued) return; + luckmailRefreshQueued = true; + setTimeout(async () => { + luckmailRefreshQueued = false; + await refreshLuckmailPurchases({ silent: true }); + }, 150); +} + +async function selectSingleLuckmailPurchase(purchase) { + setLuckmailLoadingState(true, `正在切换到 ${purchase.email_address} ...`); + try { + const response = await chrome.runtime.sendMessage({ + type: 'SELECT_LUCKMAIL_PURCHASE', + source: 'sidepanel', + payload: { purchaseId: purchase.id }, + }); + if (response?.error) throw new Error(response.error); + inputEmail.value = response?.purchase?.email_address || purchase.email_address || ''; + showToast(`已切换当前 LuckMail 邮箱为 ${purchase.email_address}`, 'success', 2200); + await refreshLuckmailPurchases({ silent: true }); + } catch (err) { + if (luckmailSummary) luckmailSummary.textContent = err.message; + showToast(`切换 LuckMail 邮箱失败:${err.message}`, 'error'); + } finally { + setLuckmailLoadingState(false); + } +} + +async function setSingleLuckmailPurchaseUsedState(purchase, used) { + setLuckmailLoadingState(true, `正在更新 ${purchase.email_address} 的已用状态...`); + try { + const response = await chrome.runtime.sendMessage({ + type: 'SET_LUCKMAIL_PURCHASE_USED_STATE', + source: 'sidepanel', + payload: { purchaseId: purchase.id, used }, + }); + if (response?.error) throw new Error(response.error); + showToast(`${purchase.email_address} 已${used ? '标记为已用' : '恢复为未用'}`, 'success', 2200); + await refreshLuckmailPurchases({ silent: true }); + } catch (err) { + if (luckmailSummary) luckmailSummary.textContent = err.message; + showToast(`更新 LuckMail 已用状态失败:${err.message}`, 'error'); + } finally { + setLuckmailLoadingState(false); + } +} + +async function setSingleLuckmailPurchasePreservedState(purchase, preserved) { + setLuckmailLoadingState(true, `正在更新 ${purchase.email_address} 的保留状态...`); + try { + const response = await chrome.runtime.sendMessage({ + type: 'SET_LUCKMAIL_PURCHASE_PRESERVED_STATE', + source: 'sidepanel', + payload: { purchaseId: purchase.id, preserved }, + }); + if (response?.error) throw new Error(response.error); + showToast(`${purchase.email_address} 已${preserved ? '设为保留' : '取消保留'}`, 'success', 2200); + await refreshLuckmailPurchases({ silent: true }); + } catch (err) { + if (luckmailSummary) luckmailSummary.textContent = err.message; + showToast(`更新 LuckMail 保留状态失败:${err.message}`, 'error'); + } finally { + setLuckmailLoadingState(false); + } +} + +async function setSingleLuckmailPurchaseDisabledState(purchase, disabled) { + setLuckmailLoadingState(true, `正在${disabled ? '禁用' : '启用'} ${purchase.email_address} ...`); + try { + const response = await chrome.runtime.sendMessage({ + type: 'SET_LUCKMAIL_PURCHASE_DISABLED_STATE', + source: 'sidepanel', + payload: { purchaseId: purchase.id, disabled }, + }); + if (response?.error) throw new Error(response.error); + showToast(`${purchase.email_address} 已${disabled ? '禁用' : '启用'}`, 'success', 2200); + await refreshLuckmailPurchases({ silent: true }); + } catch (err) { + if (luckmailSummary) luckmailSummary.textContent = err.message; + showToast(`更新 LuckMail 禁用状态失败:${err.message}`, 'error'); + } finally { + setLuckmailLoadingState(false); + } +} + +async function runBulkLuckmailAction(action) { + const selectedIds = lastRenderedLuckmailPurchases + .filter((purchase) => luckmailSelectedPurchaseIds.has(String(purchase.id))) + .map((purchase) => purchase.id); + if (!selectedIds.length) { + updateLuckmailBulkUI(); + return; + } + + const actionLabelMap = { + used: '标记已用', + unused: '标记未用', + preserve: '保留', + unpreserve: '取消保留', + disable: '禁用', + enable: '启用', + }; + + setLuckmailLoadingState(true, `正在批量${actionLabelMap[action] || '处理'} LuckMail 邮箱...`); + try { + const response = await chrome.runtime.sendMessage({ + type: 'BATCH_UPDATE_LUCKMAIL_PURCHASES', + source: 'sidepanel', + payload: { action, ids: selectedIds }, + }); + if (response?.error) throw new Error(response.error); + showToast(`已批量${actionLabelMap[action] || '处理'} ${selectedIds.length} 个 LuckMail 邮箱`, 'success', 2400); + await refreshLuckmailPurchases({ silent: true }); + } catch (err) { + if (luckmailSummary) luckmailSummary.textContent = err.message; + showToast(`批量处理 LuckMail 邮箱失败:${err.message}`, 'error'); + } finally { + setLuckmailLoadingState(false); + updateLuckmailBulkUI(); + } +} + +async function disableUsedLuckmailPurchases() { + const confirmed = await openConfirmModal({ + title: '禁用已用 LuckMail 邮箱', + message: '确认禁用所有本地已用且未保留的 openai LuckMail 邮箱吗?', + confirmLabel: '确认禁用', + confirmVariant: 'btn-danger', + }); + if (!confirmed) { + return; + } + + setLuckmailLoadingState(true, '正在禁用已用 LuckMail 邮箱...'); + try { + const response = await chrome.runtime.sendMessage({ + type: 'DISABLE_USED_LUCKMAIL_PURCHASES', + source: 'sidepanel', + payload: {}, + }); + if (response?.error) throw new Error(response.error); + const disabledCount = Array.isArray(response?.disabledIds) ? response.disabledIds.length : 0; + showToast(`已禁用 ${disabledCount} 个 LuckMail 邮箱`, disabledCount > 0 ? 'success' : 'info', 2400); + await refreshLuckmailPurchases({ silent: true }); + } catch (err) { + if (luckmailSummary) luckmailSummary.textContent = err.message; + showToast(`禁用已用 LuckMail 邮箱失败:${err.message}`, 'error'); + } finally { + setLuckmailLoadingState(false); + } +} + function getMailProviderLoginConfig(provider = selectMailProvider.value) { return MAIL_PROVIDER_LOGIN_CONFIGS[String(provider || '').trim()] || null; } @@ -1567,6 +2054,17 @@ function isCurrentEmailManagedByHotmail(state = latestState) { return inputEmailValue === hotmailEmail || stateEmailValue === hotmailEmail; } +function isCurrentEmailManagedByLuckmail(state = latestState) { + const luckmailEmail = getCurrentLuckmailEmail(state); + if (!luckmailEmail) { + return false; + } + + const inputEmailValue = String(inputEmail.value || '').trim(); + const stateEmailValue = String(state?.email || '').trim(); + return inputEmailValue === luckmailEmail || stateEmailValue === luckmailEmail; +} + function isCurrentEmailManagedByGeneratedAlias(provider = latestState?.mailProvider, state = latestState) { const normalizedProvider = String(provider || '').trim(); if (!usesGeneratedAliasMailProvider(normalizedProvider)) { @@ -1806,8 +2304,9 @@ function updateMailProviderUI() { const useGeneratedAlias = usesGeneratedAliasMailProvider(selectMailProvider.value); const useInbucket = selectMailProvider.value === 'inbucket'; const useHotmail = selectMailProvider.value === 'hotmail-api'; + const useLuckmail = isLuckmailProvider(); const useCustomEmail = isCustomMailProvider(); - const useEmailGenerator = !useHotmail && !useGeneratedAlias && !useCustomEmail; + const useEmailGenerator = !useHotmail && !useLuckmail && !useGeneratedAlias && !useCustomEmail; updateMailLoginButtonState(); rowEmailPrefix.style.display = useGeneratedAlias ? '' : 'none'; const hotmailServiceMode = getSelectedHotmailServiceMode(); @@ -1829,9 +2328,12 @@ function updateMailProviderUI() { if (hotmailSection) { hotmailSection.style.display = useHotmail ? '' : 'none'; } + if (luckmailSection) { + luckmailSection.style.display = useLuckmail ? '' : 'none'; + } labelEmailPrefix.textContent = '邮箱前缀'; inputEmailPrefix.placeholder = '例如 abc'; - selectEmailGenerator.disabled = useHotmail || useGeneratedAlias || useCustomEmail; + selectEmailGenerator.disabled = useHotmail || useLuckmail || useGeneratedAlias || useCustomEmail; if (rowHotmailServiceMode) { rowHotmailServiceMode.style.display = useHotmail ? '' : 'none'; } @@ -1841,27 +2343,36 @@ function updateMailProviderUI() { if (rowHotmailLocalBaseUrl) { rowHotmailLocalBaseUrl.style.display = useHotmail && hotmailServiceMode === HOTMAIL_SERVICE_MODE_LOCAL ? '' : 'none'; } - btnFetchEmail.hidden = useHotmail || useCustomEmail; - inputEmail.readOnly = useHotmail || useGeneratedAlias; + btnFetchEmail.hidden = useHotmail || useLuckmail || useCustomEmail; + inputEmail.readOnly = useHotmail || useLuckmail || useGeneratedAlias; const uiCopy = useCustomEmail ? getCustomMailProviderUiCopy() : getEmailGeneratorUiCopy(); inputEmail.placeholder = useHotmail ? '由 Hotmail 账号池自动分配' - : (use2925 ? '步骤 3 自动生成 2925 邮箱并回填' : uiCopy.placeholder); - btnFetchEmail.disabled = useGeneratedAlias || useCustomEmail || isAutoRunLockedPhase(); + : (useLuckmail + ? '步骤 3 自动购买 LuckMail 邮箱并回填' + : (use2925 ? '步骤 3 自动生成 2925 邮箱并回填' : uiCopy.placeholder)); + btnFetchEmail.disabled = useGeneratedAlias || useLuckmail || useCustomEmail || isAutoRunLockedPhase(); if (!btnFetchEmail.disabled) { btnFetchEmail.textContent = uiCopy.buttonLabel; } if (autoHintText) { autoHintText.textContent = useHotmail ? '请先校验并选择一个 Hotmail 账号' + : (useLuckmail + ? '步骤 3 会自动购买 LuckMail 邮箱并用于收码' : (useGeneratedAlias ? '步骤 3 会自动生成邮箱,无需手动获取' - : (useCustomEmail ? '请先填写自定义注册邮箱,成功一轮后会自动清空' : '先自动获取邮箱,或手动粘贴邮箱后再继续')); + : (useCustomEmail ? '请先填写自定义注册邮箱,成功一轮后会自动清空' : '先自动获取邮箱,或手动粘贴邮箱后再继续'))); } if (useHotmail) { inputEmail.value = getCurrentHotmailEmail(); + } else if (useLuckmail) { + inputEmail.value = getCurrentLuckmailEmail(); } renderHotmailAccounts(); + if (useLuckmail) { + renderLuckmailPurchases(lastRenderedLuckmailPurchases); + } } async function saveCloudflareDomainSettings(domains, activeDomain, options = {}) { @@ -2397,7 +2908,7 @@ document.querySelectorAll('.step-btn').forEach(btn => { syncLatestState({ customPassword: inputPassword.value }); } let email = inputEmail.value.trim(); - if (selectMailProvider.value === 'hotmail-api') { + if (selectMailProvider.value === 'hotmail-api' || isLuckmailProvider()) { const response = await chrome.runtime.sendMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step } }); if (response?.error) { throw new Error(response.error); @@ -2444,7 +2955,7 @@ document.querySelectorAll('.step-btn').forEach(btn => { }); btnFetchEmail.addEventListener('click', async () => { - if (selectMailProvider.value === 'hotmail-api' || isCustomMailProvider()) { + if (selectMailProvider.value === 'hotmail-api' || isLuckmailProvider() || isCustomMailProvider()) { return; } await fetchGeneratedEmail().catch(() => { }); @@ -2908,7 +3419,13 @@ btnReset.addEventListener('click', async () => { } await chrome.runtime.sendMessage({ type: 'RESET', source: 'sidepanel' }); - syncLatestState({ stepStatuses: STEP_DEFAULT_STATUSES, currentHotmailAccountId: null, email: null }); + syncLatestState({ + stepStatuses: STEP_DEFAULT_STATUSES, + currentHotmailAccountId: null, + currentLuckmailPurchase: null, + currentLuckmailMailCursor: null, + email: null, + }); syncAutoRunState({ autoRunning: false, autoRunPhase: 'idle', @@ -2937,6 +3454,10 @@ btnReset.addEventListener('click', async () => { updateButtonStates(); updateProgressCounter(); renderHotmailAccounts(); + renderLuckmailPurchases(lastRenderedLuckmailPurchases); + if (isLuckmailProvider()) { + queueLuckmailPurchaseRefresh(); + } }); // Clear log @@ -2946,7 +3467,7 @@ btnClearLog.addEventListener('click', () => { // Save settings on change inputEmail.addEventListener('change', async () => { - if (selectMailProvider.value === 'hotmail-api') { + if (selectMailProvider.value === 'hotmail-api' || isLuckmailProvider()) { return; } const email = inputEmail.value.trim(); @@ -2991,6 +3512,73 @@ inputVpsPassword.addEventListener('blur', () => { }); }); +[inputLuckmailApiKey, inputLuckmailBaseUrl, inputLuckmailDomain].forEach((input) => { + input?.addEventListener('input', () => { + markSettingsDirty(true); + scheduleSettingsAutoSave(); + }); + input?.addEventListener('blur', () => { + saveSettings({ silent: true }).catch(() => { }); + }); +}); + +selectLuckmailEmailType?.addEventListener('change', () => { + markSettingsDirty(true); + saveSettings({ silent: true }).catch(() => { }); +}); + +inputLuckmailSearch?.addEventListener('input', (event) => { + luckmailSearchTerm = event.target.value || ''; + renderLuckmailPurchases(lastRenderedLuckmailPurchases); +}); + +selectLuckmailFilter?.addEventListener('change', (event) => { + luckmailFilterMode = String(event.target.value || 'all').trim() || 'all'; + renderLuckmailPurchases(lastRenderedLuckmailPurchases); +}); + +checkboxLuckmailSelectAll?.addEventListener('change', () => { + const visiblePurchases = getFilteredLuckmailPurchases(); + if (checkboxLuckmailSelectAll.checked) { + visiblePurchases.forEach((purchase) => luckmailSelectedPurchaseIds.add(String(purchase.id))); + } else { + visiblePurchases.forEach((purchase) => luckmailSelectedPurchaseIds.delete(String(purchase.id))); + } + renderLuckmailPurchases(lastRenderedLuckmailPurchases); +}); + +btnLuckmailRefresh?.addEventListener('click', async () => { + await refreshLuckmailPurchases(); +}); + +btnLuckmailDisableUsed?.addEventListener('click', async () => { + await disableUsedLuckmailPurchases(); +}); + +btnLuckmailBulkUsed?.addEventListener('click', async () => { + await runBulkLuckmailAction('used'); +}); + +btnLuckmailBulkUnused?.addEventListener('click', async () => { + await runBulkLuckmailAction('unused'); +}); + +btnLuckmailBulkPreserve?.addEventListener('click', async () => { + await runBulkLuckmailAction('preserve'); +}); + +btnLuckmailBulkUnpreserve?.addEventListener('click', async () => { + await runBulkLuckmailAction('unpreserve'); +}); + +btnLuckmailBulkDisable?.addEventListener('click', async () => { + await runBulkLuckmailAction('disable'); +}); + +btnLuckmailBulkEnable?.addEventListener('click', async () => { + await runBulkLuckmailAction('enable'); +}); + inputPassword.addEventListener('input', () => { markSettingsDirty(true); updateButtonStates(); @@ -3007,12 +3595,18 @@ selectMailProvider.addEventListener('change', async () => { const leavingHotmail = previousProvider === 'hotmail-api' && nextProvider !== 'hotmail-api' && isCurrentEmailManagedByHotmail(); + const leavingLuckmail = previousProvider === LUCKMAIL_PROVIDER + && nextProvider !== LUCKMAIL_PROVIDER + && isCurrentEmailManagedByLuckmail(); const leavingGeneratedAlias = previousProvider !== nextProvider && usesGeneratedAliasMailProvider(previousProvider) && isCurrentEmailManagedByGeneratedAlias(previousProvider); - if (leavingHotmail || leavingGeneratedAlias) { + if (leavingHotmail || leavingLuckmail || leavingGeneratedAlias) { await clearRegistrationEmail({ silent: true }).catch(() => { }); } + if (nextProvider === LUCKMAIL_PROVIDER) { + queueLuckmailPurchaseRefresh(); + } markSettingsDirty(true); saveSettings({ silent: true }).catch(() => { }); }); @@ -3308,6 +3902,25 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { inputEmail.value = getCurrentHotmailEmail(); } } + if (message.payload.luckmailApiKey !== undefined) { + inputLuckmailApiKey.value = message.payload.luckmailApiKey || ''; + } + if (message.payload.luckmailBaseUrl !== undefined) { + inputLuckmailBaseUrl.value = normalizeLuckmailBaseUrl(message.payload.luckmailBaseUrl); + } + if (message.payload.luckmailEmailType !== undefined) { + selectLuckmailEmailType.value = normalizeLuckmailEmailType(message.payload.luckmailEmailType); + } + if (message.payload.luckmailDomain !== undefined) { + inputLuckmailDomain.value = message.payload.luckmailDomain || ''; + } + if (message.payload.luckmailUsedPurchases !== undefined && isLuckmailProvider()) { + queueLuckmailPurchaseRefresh(); + } + if (message.payload.currentLuckmailPurchase !== undefined && isLuckmailProvider()) { + inputEmail.value = getCurrentLuckmailEmail(); + queueLuckmailPurchaseRefresh(); + } if (message.payload.autoRunSkipFailures !== undefined) { inputAutoSkipFailures.checked = Boolean(message.payload.autoRunSkipFailures); updateFallbackThreadIntervalInputState(); diff --git a/tests/background-luckmail.test.js b/tests/background-luckmail.test.js new file mode 100644 index 0000000..d67942e --- /dev/null +++ b/tests/background-luckmail.test.js @@ -0,0 +1,568 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); + +const source = fs.readFileSync('background.js', 'utf8'); + +function extractFunction(name) { + const markers = [`async function ${name}(`, `function ${name}(`]; + const start = markers + .map((marker) => source.indexOf(marker)) + .find((index) => index >= 0); + if (start < 0) { + throw new Error(`missing function ${name}`); + } + + let parenDepth = 0; + let signatureEnded = false; + let braceStart = -1; + for (let i = start; i < source.length; i += 1) { + const ch = source[i]; + if (ch === '(') { + parenDepth += 1; + } else if (ch === ')') { + parenDepth -= 1; + if (parenDepth === 0) { + signatureEnded = true; + } + } else if (ch === '{' && signatureEnded) { + braceStart = i; + break; + } + } + + if (braceStart < 0) { + throw new Error(`missing body for function ${name}`); + } + + let depth = 0; + let end = braceStart; + for (; end < source.length; end += 1) { + const ch = source[end]; + if (ch === '{') depth += 1; + if (ch === '}') { + depth -= 1; + if (depth === 0) { + end += 1; + break; + } + } + } + + return source.slice(start, end); +} + +test('ensureLuckmailPurchaseForFlow buys openai mailbox and defaults email type to ms_graph', async () => { + const bundle = [ + extractFunction('getLuckmailSessionConfig'), + extractFunction('getCurrentLuckmailPurchase'), + extractFunction('ensureLuckmailPurchaseForFlow'), + ].join('\n'); + + const factory = new Function('initialState', ` +let currentState = { ...initialState }; +const DEFAULT_LUCKMAIL_PROJECT_CODE = 'openai'; +const purchaseCalls = []; +const activateCalls = []; + +function normalizeLuckmailBaseUrl(value) { + return String(value || '').trim() || 'https://mails.luckyous.com'; +} +function normalizeLuckmailEmailType(value) { + return ['self_built', 'ms_imap', 'ms_graph', 'google_variant'].includes(String(value || '').trim()) + ? String(value || '').trim() + : 'ms_graph'; +} +function normalizeLuckmailPurchase(value) { + return value; +} +function normalizeLuckmailPurchases(value) { + return value.purchases || []; +} +async function getState() { + return currentState; +} +function createLuckmailClient() { + return { + user: { + async purchaseEmails(projectCode, quantity, options) { + purchaseCalls.push({ projectCode, quantity, options }); + return { + purchases: [{ id: 15, email_address: 'demo@outlook.com', token: 'tok-1' }], + }; + }, + }, + }; +} +async function findReusableLuckmailPurchaseForFlow() { + return null; +} +async function activateLuckmailPurchaseForFlow(state, client, purchase, options) { + activateCalls.push({ state, purchase, options }); + currentState.currentLuckmailPurchase = purchase; + currentState.email = purchase.email_address; + return purchase; +} + +${bundle} + +return { + ensureLuckmailPurchaseForFlow, + snapshot() { + return { currentState, purchaseCalls, activateCalls }; + }, +}; +`); + + const api = factory({ + luckmailApiKey: 'sk-test', + luckmailBaseUrl: '', + luckmailEmailType: '', + luckmailDomain: '', + currentLuckmailPurchase: null, + email: null, + }); + + const purchase = await api.ensureLuckmailPurchaseForFlow(); + const snapshot = api.snapshot(); + + assert.equal(purchase.email_address, 'demo@outlook.com'); + assert.deepStrictEqual(snapshot.purchaseCalls, [{ + projectCode: 'openai', + quantity: 1, + options: { + emailType: 'ms_graph', + domain: undefined, + }, + }]); + assert.equal(snapshot.activateCalls[0].options.initializeCursor, false); + assert.equal(snapshot.currentState.email, 'demo@outlook.com'); +}); + +test('ensureLuckmailPurchaseForFlow reuses reusable openai mailbox before buying a new one', async () => { + const bundle = [ + extractFunction('getLuckmailSessionConfig'), + extractFunction('getCurrentLuckmailPurchase'), + extractFunction('ensureLuckmailPurchaseForFlow'), + ].join('\n'); + + const factory = new Function('initialState', ` +let currentState = { ...initialState }; +const DEFAULT_LUCKMAIL_PROJECT_CODE = 'openai'; +const purchaseCalls = []; +const activateCalls = []; + +function normalizeLuckmailBaseUrl(value) { + return String(value || '').trim() || 'https://mails.luckyous.com'; +} +function normalizeLuckmailEmailType(value) { + return ['self_built', 'ms_imap', 'ms_graph', 'google_variant'].includes(String(value || '').trim()) + ? String(value || '').trim() + : 'ms_graph'; +} +function normalizeLuckmailPurchase(value) { + return value; +} +function normalizeLuckmailPurchases(value) { + return value.purchases || []; +} +async function getState() { + return currentState; +} +function createLuckmailClient() { + return { + user: { + async purchaseEmails(projectCode, quantity, options) { + purchaseCalls.push({ projectCode, quantity, options }); + return { purchases: [] }; + }, + }, + }; +} +async function findReusableLuckmailPurchaseForFlow() { + return { + id: 99, + email_address: 'reuse@outlook.com', + token: 'tok-reuse', + }; +} +async function activateLuckmailPurchaseForFlow(state, client, purchase, options) { + activateCalls.push({ state, purchase, options }); + currentState.currentLuckmailPurchase = purchase; + currentState.email = purchase.email_address; + return purchase; +} + +${bundle} + +return { + ensureLuckmailPurchaseForFlow, + snapshot() { + return { currentState, purchaseCalls, activateCalls }; + }, +}; +`); + + const api = factory({ + luckmailApiKey: 'sk-test', + luckmailBaseUrl: 'https://mails.luckyous.com', + luckmailEmailType: 'ms_imap', + luckmailDomain: 'outlook.com', + currentLuckmailPurchase: null, + email: null, + }); + + const purchase = await api.ensureLuckmailPurchaseForFlow(); + const snapshot = api.snapshot(); + + assert.equal(purchase.id, 99); + assert.deepStrictEqual(snapshot.purchaseCalls, []); + assert.equal(snapshot.activateCalls[0].options.initializeCursor, true); + assert.match(snapshot.activateCalls[0].options.logMessage, /已复用 openai 邮箱/); +}); + +test('activateLuckmailPurchaseForFlow builds baseline cursor from existing mails when reusing mailbox', async () => { + const bundle = extractFunction('activateLuckmailPurchaseForFlow'); + + const factory = new Function(` +let currentPurchase = null; +let currentCursor = null; +let currentEmail = null; +const buildCalls = []; + +function normalizeLuckmailPurchase(value) { + return value; +} +async function setLuckmailPurchaseState(value) { + currentPurchase = value; +} +async function setLuckmailMailCursorState(value) { + currentCursor = value; +} +async function setEmailState(value) { + currentEmail = value; +} +async function addLog() {} +function buildLuckmailBaselineCursor(mails) { + buildCalls.push(mails); + return { messageId: 'mail-new', receivedAt: '2026-04-14 13:32:05' }; +} + +${bundle} + +return { + activateLuckmailPurchaseForFlow, + snapshot() { + return { currentPurchase, currentCursor, currentEmail, buildCalls }; + }, +}; +`); + + const api = factory(); + const client = { + user: { + async getTokenMails() { + return { + mails: [ + { message_id: 'mail-old', received_at: '2026-04-14 13:31:15' }, + { message_id: 'mail-new', received_at: '2026-04-14 13:32:05' }, + ], + }; + }, + }, + }; + + await api.activateLuckmailPurchaseForFlow({}, client, { + id: 5, + email_address: 'reuse@outlook.com', + token: 'tok-reuse', + }, { + initializeCursor: true, + logMessage: 'reuse', + }); + + const snapshot = api.snapshot(); + assert.equal(snapshot.currentPurchase.id, 5); + assert.deepStrictEqual(snapshot.currentCursor, { + messageId: 'mail-new', + receivedAt: '2026-04-14 13:32:05', + }); + assert.equal(snapshot.currentEmail, 'reuse@outlook.com'); + assert.equal(snapshot.buildCalls.length, 1); +}); + +test('listLuckmailPurchasesByProject only keeps openai purchases', async () => { + const bundle = extractFunction('listLuckmailPurchasesByProject'); + + const factory = new Function(` +const DEFAULT_LUCKMAIL_PROJECT_CODE = 'openai'; +function normalizeLuckmailProjectName(value) { + return String(value || '').trim().toLowerCase(); +} +function isLuckmailPurchaseForProject(purchase, projectCode) { + return normalizeLuckmailProjectName(purchase.project_name || purchase.project) === normalizeLuckmailProjectName(projectCode); +} +async function getAllLuckmailPurchases() { + return [ + { id: 1, project_name: 'OpenAi' }, + { id: 2, project_name: 'other' }, + { id: 3, project: 'openai' }, + ]; +} + +${bundle} + +return { listLuckmailPurchasesByProject }; +`); + + const api = factory(); + const result = await api.listLuckmailPurchasesByProject({}, { projectCode: 'openai' }); + assert.deepStrictEqual(result.map((item) => item.id), [1, 3]); +}); + +test('disableUsedLuckmailPurchases only disables locally used and non-preserved openai mailboxes', async () => { + const bundle = extractFunction('disableUsedLuckmailPurchases'); + + const factory = new Function(` +let clearedOptions = null; +const disabledCalls = []; +const DEFAULT_LUCKMAIL_PROJECT_CODE = 'openai'; + +function normalizeLuckmailPurchaseId(value) { + const numeric = Number(value); + return Number.isFinite(numeric) && numeric > 0 ? String(Math.floor(numeric)) : ''; +} +async function ensureManualInteractionAllowed() { + return { + luckmailUsedPurchases: { 1: true, 2: true, 3: true }, + luckmailPreserveTagId: 9, + luckmailPreserveTagName: '保留', + mailProvider: 'luckmail-api', + }; +} +function getLuckmailUsedPurchases(state) { + return state.luckmailUsedPurchases; +} +function getLuckmailPreserveTagInfo(state) { + return { + id: state.luckmailPreserveTagId, + name: state.luckmailPreserveTagName, + }; +} +function isLuckmailPurchasePreserved(purchase, options) { + return purchase.tag_id === options.preserveTagId || purchase.tag_name === options.preserveTagName; +} +function createLuckmailClient() { + return { + user: { + async batchSetPurchaseDisabled(ids, disabled) { + disabledCalls.push({ ids, disabled }); + }, + }, + }; +} +async function listLuckmailPurchasesByProject() { + return [ + { id: 1, email_address: 'used-1@outlook.com', user_disabled: 0, tag_id: 0, tag_name: '' }, + { id: 2, email_address: 'preserved@outlook.com', user_disabled: 0, tag_id: 9, tag_name: '保留' }, + { id: 3, email_address: 'already-disabled@outlook.com', user_disabled: 1, tag_id: 0, tag_name: '' }, + { id: 4, email_address: 'unused@outlook.com', user_disabled: 0, tag_id: 0, tag_name: '' }, + ]; +} +async function getState() { + return { + currentLuckmailPurchase: { id: 1 }, + mailProvider: 'luckmail-api', + }; +} +function getCurrentLuckmailPurchase(state) { + return state.currentLuckmailPurchase; +} +function isLuckmailProvider(state) { + return state.mailProvider === 'luckmail-api'; +} +async function clearLuckmailRuntimeState(options) { + clearedOptions = options; +} +async function addLog() {} + +${bundle} + +return { + disableUsedLuckmailPurchases, + snapshot() { + return { disabledCalls, clearedOptions }; + }, +}; +`); + + const api = factory(); + const result = await api.disableUsedLuckmailPurchases(); + const snapshot = api.snapshot(); + + assert.deepStrictEqual(result.disabledIds, [1]); + assert.deepStrictEqual(snapshot.disabledCalls, [{ ids: [1], disabled: 1 }]); + assert.deepStrictEqual(snapshot.clearedOptions, { clearEmail: true }); +}); + +test('resetState preserves LuckMail session config, used map, and preserve tag cache while clearing runtime purchase state', async () => { + const bundle = extractFunction('resetState'); + + const factory = new Function([ + 'let cleared = false;', + 'let storedPayload = null;', + "const LOG_PREFIX = '[test]';", + "const DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME = '保留';", + 'const DEFAULT_STATE = {', + " luckmailApiKey: '',", + " luckmailBaseUrl: 'https://mails.luckyous.com',", + " luckmailEmailType: 'ms_graph',", + " luckmailDomain: '',", + ' luckmailUsedPurchases: {},', + ' luckmailPreserveTagId: 0,', + " luckmailPreserveTagName: '保留',", + " currentLuckmailPurchase: { token: 'stale' },", + " currentLuckmailMailCursor: { messageId: 'stale' },", + ' email: null,', + '};', + 'function normalizeLuckmailBaseUrl(value) {', + " const normalized = String(value || '').trim() || 'https://mails.luckyous.com';", + " return normalized.replace(/\\/$/, '');", + '}', + 'function normalizeLuckmailEmailType(value) {', + " return ['self_built', 'ms_imap', 'ms_graph', 'google_variant'].includes(String(value || '').trim())", + " ? String(value || '').trim()", + " : 'ms_graph';", + '}', + 'function normalizeLuckmailUsedPurchases(value) {', + ' return value || {};', + '}', + 'async function getPersistedSettings() {', + " return { mailProvider: '163' };", + '}', + 'const chrome = {', + ' storage: {', + ' session: {', + ' async get() {', + ' return {', + " seenCodes: ['seen-1'],", + " seenInbucketMailIds: ['mail-1'],", + " accounts: [{ email: 'saved@example.com' }],", + " tabRegistry: { foo: { tabId: 1 } },", + " sourceLastUrls: { foo: 'https://example.com' },", + " luckmailApiKey: 'sk-session',", + " luckmailBaseUrl: 'https://demo.example.com/',", + " luckmailEmailType: 'ms_imap',", + " luckmailDomain: 'outlook.com',", + " luckmailUsedPurchases: { 88: true },", + ' luckmailPreserveTagId: 9,', + " luckmailPreserveTagName: '保留',", + ' };', + ' },', + ' async clear() {', + ' cleared = true;', + ' },', + ' async set(payload) {', + ' storedPayload = payload;', + ' },', + ' },', + ' },', + '};', + bundle, + 'return {', + ' resetState,', + ' snapshot() {', + ' return { cleared, storedPayload };', + ' },', + '};', + ].join('\n')); + + const api = factory(); + await api.resetState(); + const snapshot = api.snapshot(); + + assert.equal(snapshot.cleared, true); + assert.equal(snapshot.storedPayload.luckmailApiKey, 'sk-session'); + assert.equal(snapshot.storedPayload.luckmailBaseUrl, 'https://demo.example.com'); + assert.equal(snapshot.storedPayload.luckmailEmailType, 'ms_imap'); + assert.equal(snapshot.storedPayload.luckmailDomain, 'outlook.com'); + assert.deepStrictEqual(snapshot.storedPayload.luckmailUsedPurchases, { 88: true }); + assert.equal(snapshot.storedPayload.luckmailPreserveTagId, 9); + assert.equal(snapshot.storedPayload.luckmailPreserveTagName, '保留'); + assert.equal(snapshot.storedPayload.currentLuckmailPurchase, null); + assert.equal(snapshot.storedPayload.currentLuckmailMailCursor, null); +}); + +test('handleStepData step 9 marks current LuckMail purchase as used and clears runtime state', async () => { + const bundle = extractFunction('handleStepData'); + + const factory = new Function(` +let clearedOptions = null; +let usedMarker = null; +const logs = []; + +async function closeLocalhostCallbackTabs() {} +async function getState() { + return { + mailProvider: 'luckmail-api', + currentHotmailAccountId: null, + currentLuckmailPurchase: { + id: 123, + email_address: 'demo@outlook.com', + }, + email: 'demo@outlook.com', + }; +} +function getCurrentLuckmailPurchase(state) { + return state.currentLuckmailPurchase; +} +function isHotmailProvider() { + return false; +} +async function patchHotmailAccount() {} +function isLuckmailProvider(state) { + return state.mailProvider === 'luckmail-api'; +} +async function setLuckmailPurchaseUsedState(purchaseId, used) { + usedMarker = { purchaseId, used }; +} +async function clearLuckmailRuntimeState(options) { + clearedOptions = options; +} +async function addLog(message, level) { + logs.push({ message, level }); +} +function buildLocalhostCleanupPrefix() { + return ''; +} +async function closeTabsByUrlPrefix() {} +function shouldUseCustomRegistrationEmail() { + return false; +} +async function setEmailStateSilently() {} +async function setState() {} +function broadcastDataUpdate() {} +function isLocalhostOAuthCallbackUrl() { + return true; +} + +${bundle} + +return { + handleStepData, + snapshot() { + return { clearedOptions, usedMarker, logs }; + }, +}; +`); + + const api = factory(); + await api.handleStepData(9, { + localhostUrl: 'http://localhost:1455/auth/callback?code=abc&state=xyz', + }); + + const snapshot = api.snapshot(); + assert.deepStrictEqual(snapshot.usedMarker, { purchaseId: 123, used: true }); + assert.deepStrictEqual(snapshot.clearedOptions, { clearEmail: true }); + assert.equal(snapshot.logs.at(-1).message, '当前 LuckMail 邮箱运行态已清空,下轮将优先复用未用邮箱或重新购买邮箱。'); +}); diff --git a/tests/luckmail-utils.test.js b/tests/luckmail-utils.test.js new file mode 100644 index 0000000..1d9acbc --- /dev/null +++ b/tests/luckmail-utils.test.js @@ -0,0 +1,261 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); + +const { + DEFAULT_LUCKMAIL_BASE_URL, + DEFAULT_LUCKMAIL_EMAIL_TYPE, + DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME, + buildLuckmailBaselineCursor, + buildLuckmailMailCursor, + extractLuckmailVerificationCode, + filterReusableLuckmailPurchases, + isLuckmailMailNewerThanCursor, + isLuckmailPurchaseForProject, + normalizeLuckmailBaseUrl, + normalizeLuckmailEmailType, + normalizeLuckmailProjectName, + normalizeLuckmailPurchaseListPage, + normalizeLuckmailTags, + normalizeLuckmailUsedPurchases, + normalizeTimestamp, + normalizeLuckmailTokenCode, + normalizeLuckmailTokenMail, + pickReusableLuckmailPurchase, + pickLuckmailVerificationMail, +} = require('../luckmail-utils.js'); + +test('normalizeLuckmailEmailType keeps supported values and falls back to ms_graph', () => { + assert.equal(normalizeLuckmailEmailType('self_built'), 'self_built'); + assert.equal(normalizeLuckmailEmailType('ms_imap'), 'ms_imap'); + assert.equal(normalizeLuckmailEmailType('ms_graph'), 'ms_graph'); + assert.equal(normalizeLuckmailEmailType('google_variant'), 'google_variant'); + assert.equal(normalizeLuckmailEmailType(''), DEFAULT_LUCKMAIL_EMAIL_TYPE); + assert.equal(normalizeLuckmailEmailType('unknown'), DEFAULT_LUCKMAIL_EMAIL_TYPE); +}); + +test('normalizeLuckmailBaseUrl trims invalid input to default base url', () => { + assert.equal(normalizeLuckmailBaseUrl(''), DEFAULT_LUCKMAIL_BASE_URL); + assert.equal(normalizeLuckmailBaseUrl('https://mails.luckyous.com/'), DEFAULT_LUCKMAIL_BASE_URL); + assert.equal(normalizeLuckmailBaseUrl('https://demo.example.com/api/'), 'https://demo.example.com/api'); + assert.equal(normalizeLuckmailBaseUrl('notaurl'), DEFAULT_LUCKMAIL_BASE_URL); +}); + +test('normalizeLuckmailTokenCode and normalizeLuckmailTokenMail extract verification code', () => { + const tokenCode = normalizeLuckmailTokenCode({ + email_address: 'demo@outlook.com', + project: 'openai', + has_new_mail: true, + verification_code: '123456', + mail: { + message_id: 'mail-1', + from: 'noreply@openai.com', + subject: 'Your ChatGPT code is 123456', + received_at: '2026-04-14T10:00:00Z', + }, + }); + + assert.equal(tokenCode.verification_code, '123456'); + assert.equal(tokenCode.mail.message_id, 'mail-1'); + assert.equal(tokenCode.mail.verification_code, '123456'); + + const normalizedMail = normalizeLuckmailTokenMail({ + message_id: 'mail-2', + from: 'noreply@openai.com', + subject: 'OpenAI security message', + body: 'Your verification code is 654321.', + received_at: '2026-04-14T10:01:00Z', + }); + + assert.equal(normalizedMail.verification_code, '654321'); + assert.equal(extractLuckmailVerificationCode('你的验证码为 778899'), '778899'); +}); + +test('normalizeLuckmailProjectName and isLuckmailPurchaseForProject match openai case-insensitively', () => { + assert.equal(normalizeLuckmailProjectName(' OpenAi '), 'openai'); + assert.equal(isLuckmailPurchaseForProject({ + id: 1, + email_address: 'demo@outlook.com', + token: 'tok-1', + project_name: 'OpenAi', + }, 'openai'), true); + assert.equal(isLuckmailPurchaseForProject({ + id: 2, + email_address: 'other@example.com', + token: 'tok-2', + project: 'OtherProject', + }, 'openai'), false); +}); + +test('normalizeLuckmailPurchaseListPage and normalizeLuckmailTags normalize list payloads', () => { + const page = normalizeLuckmailPurchaseListPage({ + list: [{ + id: 3, + email_address: 'demo@outlook.com', + token: 'tok-3', + project_name: 'OpenAi', + }], + total: 4, + page: 2, + page_size: 1, + }); + assert.equal(page.total, 4); + assert.equal(page.page, 2); + assert.equal(page.page_size, 1); + assert.equal(page.list[0].project_code, 'openai'); + + const tags = normalizeLuckmailTags([{ + id: 9, + name: DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME, + limit_type: 0, + }]); + assert.deepEqual(tags[0], { + id: 9, + name: DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME, + remark: '', + limit_type: 0, + purchase_count: 0, + created_at: null, + }); +}); + +test('normalizeLuckmailUsedPurchases keeps positive numeric keys only', () => { + assert.deepEqual(normalizeLuckmailUsedPurchases({ + 1: true, + foo: true, + '-2': true, + 3: false, + }), { + 1: true, + 3: false, + }); +}); + +test('pickReusableLuckmailPurchase only returns reusable openai purchase', () => { + const purchases = [{ + id: 10, + email_address: 'used@outlook.com', + token: 'tok-used', + project_name: 'openai', + }, { + id: 11, + email_address: 'preserved@outlook.com', + token: 'tok-preserved', + project_name: 'OpenAi', + tag_name: DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME, + }, { + id: 12, + email_address: 'disabled@outlook.com', + token: 'tok-disabled', + project_name: 'openai', + user_disabled: 1, + }, { + id: 13, + email_address: 'expired@outlook.com', + token: 'tok-expired', + project_name: 'openai', + warranty_until: '2026-04-14T09:00:00Z', + }, { + id: 14, + email_address: 'other@example.com', + token: 'tok-other', + project_name: 'other', + }, { + id: 15, + email_address: 'ready@outlook.com', + token: 'tok-ready', + project_name: 'OpenAi', + warranty_until: '2026-04-15T09:00:00Z', + }]; + + const reusable = filterReusableLuckmailPurchases(purchases, { + projectCode: 'openai', + usedPurchases: { 10: true }, + preserveTagName: DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME, + now: Date.parse('2026-04-14T10:00:00Z'), + }); + + assert.deepEqual(reusable.map((purchase) => purchase.id), [15]); + assert.equal(pickReusableLuckmailPurchase(purchases, { + projectCode: 'openai', + usedPurchases: { 10: true }, + preserveTagName: DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME, + now: Date.parse('2026-04-14T10:00:00Z'), + }).id, 15); +}); + +test('pickLuckmailVerificationMail respects sender filters, time filters, and excluded codes', () => { + const match = pickLuckmailVerificationMail([ + { + message_id: 'old-mail', + from: 'noreply@openai.com', + subject: 'Your code is 111111', + received_at: '2026-04-14T09:59:00Z', + }, + { + message_id: 'new-mail', + from: 'noreply@openai.com', + subject: 'Your code is 222222', + received_at: '2026-04-14T10:05:00Z', + }, + ], { + senderFilters: ['openai'], + subjectFilters: ['code'], + excludeCodes: ['111111'], + afterTimestamp: Date.parse('2026-04-14T10:00:00Z'), + }); + + assert.equal(match.code, '222222'); + assert.equal(match.mail.message_id, 'new-mail'); +}); + +test('isLuckmailMailNewerThanCursor compares message id and timestamp safely', () => { + const cursor = buildLuckmailMailCursor({ + message_id: 'mail-1', + received_at: '2026-04-14T10:00:00Z', + }); + + assert.equal(isLuckmailMailNewerThanCursor({ + message_id: 'mail-1', + received_at: '2026-04-14T10:00:00Z', + }, cursor), false); + + assert.equal(isLuckmailMailNewerThanCursor({ + message_id: 'mail-2', + received_at: '2026-04-14T10:01:00Z', + }, cursor), true); +}); + +test('normalizeLuckmailMailCursor tolerates null cursor input', () => { + const { normalizeLuckmailMailCursor } = require('../luckmail-utils.js'); + assert.deepEqual(normalizeLuckmailMailCursor(null), { + messageId: '', + receivedAt: '', + }); +}); + +test('normalizeTimestamp treats LuckMail naive datetime strings as UTC', () => { + assert.equal( + normalizeTimestamp('2026-04-14 13:32:05'), + Date.UTC(2026, 3, 14, 13, 32, 5, 0) + ); +}); + +test('buildLuckmailBaselineCursor tracks newest existing mail as baseline', () => { + const cursor = buildLuckmailBaselineCursor([ + { + message_id: 'mail-old', + received_at: '2026-04-14 13:31:15', + subject: '你的 ChatGPT 代码为 111111', + }, + { + message_id: 'mail-new', + received_at: '2026-04-14 13:32:05', + subject: '你的 ChatGPT 代码为 222222', + }, + ]); + + assert.deepEqual(cursor, { + messageId: 'mail-new', + receivedAt: '2026-04-14 13:32:05', + }); +});