From 1947f8b561fadc079081c9147c1a4c1c315a1683 Mon Sep 17 00:00:00 2001 From: honue Date: Wed, 15 Apr 2026 16:44:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20iCloud=20=E9=82=AE?= =?UTF-8?q?=E7=AE=B1=20provider?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background.js | 16 + content/icloud-mail.js | 308 ++++++++++++++++++ content/utils.js | 1 + icloud-utils.js | 8 + manifest.json | 13 + sidepanel/sidepanel.html | 2 + sidepanel/sidepanel.js | 30 +- tests/background-icloud-mail-provider.test.js | 145 +++++++++ tests/icloud-mail-content.test.js | 181 ++++++++++ tests/icloud-utils.test.js | 2 + tests/sidepanel-icloud-provider.test.js | 81 +++++ 11 files changed, 785 insertions(+), 2 deletions(-) create mode 100644 content/icloud-mail.js create mode 100644 tests/background-icloud-mail-provider.test.js create mode 100644 tests/icloud-mail-content.test.js create mode 100644 tests/sidepanel-icloud-provider.test.js diff --git a/background.js b/background.js index 8a37557..d2ca938 100644 --- a/background.js +++ b/background.js @@ -71,6 +71,7 @@ const { getConfiguredIcloudHostPreference, getIcloudHostHintFromMessage, getIcloudLoginUrlForHost, + getIcloudMailUrlForHost, getIcloudSetupUrlForHost, normalizeBooleanMap, normalizeIcloudAliasList, @@ -92,6 +93,7 @@ const ICLOUD_LOGIN_URLS = [ 'https://www.icloud.com.cn/', 'https://www.icloud.com/', ]; +const ICLOUD_PROVIDER = 'icloud'; const GMAIL_PROVIDER = 'gmail'; const HOTMAIL_PROVIDER = 'hotmail-api'; const LUCKMAIL_PROVIDER = 'luckmail-api'; @@ -489,6 +491,7 @@ function normalizeMailProvider(value = '') { const normalized = String(value || '').trim().toLowerCase(); switch (normalized) { case 'custom': + case ICLOUD_PROVIDER: case GMAIL_PROVIDER: case HOTMAIL_PROVIDER: case LUCKMAIL_PROVIDER: @@ -6606,6 +6609,19 @@ function getMailConfig(state) { if (provider === HOTMAIL_PROVIDER) { return { provider: HOTMAIL_PROVIDER, label: 'Hotmail(API对接/本地助手)' }; } + if (provider === ICLOUD_PROVIDER) { + const configuredHost = getConfiguredIcloudHostPreference(state) + || normalizeIcloudHost(state?.preferredIcloudHost) + || 'icloud.com'; + const loginUrl = getIcloudLoginUrlForHost(configuredHost) || 'https://www.icloud.com/'; + const mailUrl = getIcloudMailUrlForHost(configuredHost) || loginUrl; + return { + source: 'icloud-mail', + url: mailUrl, + label: 'iCloud 邮箱', + navigateOnReuse: true, + }; + } if (provider === GMAIL_PROVIDER) { return { source: 'gmail-mail', diff --git a/content/icloud-mail.js b/content/icloud-mail.js new file mode 100644 index 0000000..35c6d44 --- /dev/null +++ b/content/icloud-mail.js @@ -0,0 +1,308 @@ +const ICLOUD_MAIL_PREFIX = '[MultiPage:icloud-mail]'; +const isTopFrame = window === window.top; + +console.log(ICLOUD_MAIL_PREFIX, 'Content script loaded on', location.href, 'frame:', isTopFrame ? 'top' : 'child'); + +function isMailApplicationFrame() { + if (/\/applications\/mail2\//.test(location.pathname)) { + return true; + } + return Boolean(document.querySelector('.content-container, .mail-message-defaults, .thread-participants')); +} + +if (isTopFrame) { + console.log(ICLOUD_MAIL_PREFIX, 'Top frame detected; waiting for mail iframe.'); +} else { + chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.type === 'POLL_EMAIL') { + if (!isMailApplicationFrame()) { + sendResponse({ ok: false, reason: 'wrong-frame' }); + return; + } + resetStopState(); + handlePollEmail(message.step, message.payload).then((result) => { + sendResponse(result); + }).catch((err) => { + if (isStopError(err)) { + log(`步骤 ${message.step}:已被用户停止。`, 'warn'); + sendResponse({ stopped: true, error: err.message }); + return; + } + log(`步骤 ${message.step}:iCloud 邮箱轮询失败:${err.message}`, 'warn'); + sendResponse({ error: err.message }); + }); + return true; + } + }); + + function normalizeText(value) { + return String(value || '').replace(/\s+/g, ' ').trim(); + } + + function isVisibleElement(node) { + return Boolean(node instanceof HTMLElement) + && (Boolean(node.offsetParent) || getComputedStyle(node).position === 'fixed'); + } + + function collectThreadItems() { + return Array.from(document.querySelectorAll('.content-container')).filter((item) => { + if (!isVisibleElement(item)) return false; + return item.querySelector('.thread-participants') + && item.querySelector('.thread-subject') + && item.querySelector('.thread-preview'); + }); + } + + function getThreadItemMetadata(item) { + const sender = normalizeText(item.querySelector('.thread-participants')?.textContent || ''); + const subject = normalizeText(item.querySelector('.thread-subject')?.textContent || ''); + const preview = normalizeText(item.querySelector('.thread-preview')?.textContent || ''); + const timestamp = normalizeText(item.querySelector('.thread-timestamp')?.textContent || ''); + return { + sender, + subject, + preview, + timestamp, + combinedText: normalizeText([sender, subject, preview, timestamp].filter(Boolean).join(' ')), + }; + } + + function buildItemSignature(item) { + const meta = getThreadItemMetadata(item); + return normalizeText([ + item.getAttribute('aria-label') || '', + meta.sender, + meta.subject, + meta.preview, + meta.timestamp, + ].join('::')).slice(0, 240); + } + + function extractVerificationCode(text) { + const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/); + if (matchCn) return matchCn[1]; + + const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i); + if (matchEn) return matchEn[1] || matchEn[2]; + + const match6 = text.match(/\b(\d{6})\b/); + if (match6) return match6[1]; + + return null; + } + + function readOpenedMailHeader() { + const headerRoot = document.querySelector('.ic-efwqa7'); + if (!headerRoot) { + return { sender: '', recipients: '', timestamp: '' }; + } + + const contactValues = Array.from(headerRoot.querySelectorAll('.contact-token .ic-x1z554')) + .map((node) => normalizeText(node.textContent)) + .filter(Boolean); + const sender = contactValues[0] || ''; + const recipients = contactValues.slice(1).join(' '); + const timestamp = normalizeText(headerRoot.querySelector('.ic-rffsj8')?.textContent || ''); + return { sender, recipients, timestamp }; + } + + function getOpenedMailBodyRoot() { + return document.querySelector('.mail-message-defaults, .pane.thread-detail-pane'); + } + + function readOpenedMailBody() { + const bodyRoot = getOpenedMailBodyRoot(); + return normalizeText(bodyRoot?.innerText || bodyRoot?.textContent || ''); + } + + function getThreadListItemRoot(item) { + return item?.closest?.('.thread-list-item, [role="treeitem"]') || null; + } + + function isThreadItemSelected(item, expectedSignature = '') { + const expected = normalizeText(expectedSignature); + const candidates = collectThreadItems(); + const matchedItem = expected + ? candidates.find((candidate) => buildItemSignature(candidate) === expected) + : item; + const root = getThreadListItemRoot(matchedItem || item); + if (!root) { + return false; + } + if (root.getAttribute('aria-selected') === 'true') { + return true; + } + const className = String(root.className || '').toLowerCase(); + return /\b(selected|current|active)\b/.test(className); + } + + function openedMailMatchesExpectedContent(expectedMeta = {}, header = null, bodyText = '') { + const expectedSender = normalizeText(expectedMeta.sender || '').toLowerCase(); + const expectedSubject = normalizeText(expectedMeta.subject || '').toLowerCase(); + const combined = normalizeText([ + header?.sender || '', + header?.recipients || '', + header?.timestamp || '', + bodyText || '', + ].join(' ')).toLowerCase(); + + if (expectedSender && combined.includes(expectedSender)) { + return true; + } + if (expectedSubject && combined.includes(expectedSubject)) { + return true; + } + return false; + } + + async function waitForOpenedMailContent(item, expectedMeta = {}, timeout = 10000) { + const expectedSignature = buildItemSignature(item); + const start = Date.now(); + while (Date.now() - start < timeout) { + throwIfStopped(); + const headerRoot = document.querySelector('.ic-efwqa7'); + const bodyRoot = getOpenedMailBodyRoot(); + const selected = isThreadItemSelected(item, expectedSignature); + if (selected && (headerRoot || bodyRoot)) { + const header = readOpenedMailHeader(); + const bodyText = normalizeText(bodyRoot?.innerText || bodyRoot?.textContent || ''); + if (openedMailMatchesExpectedContent(expectedMeta, header, bodyText)) { + return { headerRoot, bodyRoot }; + } + } + await sleep(100); + } + throw new Error('打开邮件后未找到详情区域,请确认邮件内容已加载。'); + } + + async function openMailItemAndRead(item) { + const expectedMeta = getThreadItemMetadata(item); + simulateClick(item); + + const { bodyRoot } = await waitForOpenedMailContent(item, expectedMeta, 10000); + await sleep(300); + + const header = readOpenedMailHeader(); + const bodyText = normalizeText( + bodyRoot?.innerText || bodyRoot?.textContent || readOpenedMailBody() + ); + return { + ...header, + bodyText, + combinedText: normalizeText([header.sender, header.recipients, header.timestamp, bodyText].filter(Boolean).join(' ')), + }; + } + + async function refreshInbox() { + const refreshPatterns = [/刷新/i, /refresh/i, /重新载入/i]; + const candidates = document.querySelectorAll('button, [role="button"], a'); + for (const node of candidates) { + const text = normalizeText(node.innerText || node.textContent || ''); + const label = normalizeText(node.getAttribute('aria-label') || node.getAttribute('title') || ''); + if (refreshPatterns.some((pattern) => pattern.test(text) || pattern.test(label))) { + simulateClick(node); + await sleep(1000); + return; + } + } + + const inboxPatterns = [/收件箱/, /inbox/i]; + for (const node of candidates) { + const text = normalizeText(node.innerText || node.textContent || ''); + const label = normalizeText(node.getAttribute('aria-label') || node.getAttribute('title') || ''); + if (inboxPatterns.some((pattern) => pattern.test(text) || pattern.test(label))) { + simulateClick(node); + await sleep(1000); + return; + } + } + } + + async function handlePollEmail(step, payload) { + const { senderFilters, subjectFilters, maxAttempts, intervalMs, excludeCodes = [] } = payload; + const excludedCodeSet = new Set(excludeCodes.filter(Boolean)); + const FALLBACK_AFTER = 3; + const normalizedSenderFilters = senderFilters.map((filter) => String(filter || '').toLowerCase()).filter(Boolean); + const normalizedSubjectFilters = subjectFilters.map((filter) => String(filter || '').toLowerCase()).filter(Boolean); + + log(`步骤 ${step}:开始轮询 iCloud 邮箱(最多 ${maxAttempts} 次)`); + await waitForElement('.content-container', 10000); + await sleep(1500); + + const existingSignatures = new Set(collectThreadItems().map(buildItemSignature)); + log(`步骤 ${step}:已记录当前 ${existingSignatures.size} 封旧邮件快照`); + + for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { + log(`步骤 ${step}:正在轮询 iCloud 邮箱,第 ${attempt}/${maxAttempts} 次`); + + if (attempt > 1) { + await refreshInbox(); + await sleep(1200); + } + + const items = collectThreadItems(); + const useFallback = attempt > FALLBACK_AFTER; + + for (const item of items) { + const signature = buildItemSignature(item); + if (!useFallback && existingSignatures.has(signature)) { + continue; + } + + const meta = getThreadItemMetadata(item); + const lowerSender = meta.sender.toLowerCase(); + const lowerSubject = normalizeText([meta.subject, meta.preview].join(' ')).toLowerCase(); + const senderMatch = normalizedSenderFilters.some((filter) => lowerSender.includes(filter)); + const subjectMatch = normalizedSubjectFilters.some((filter) => lowerSubject.includes(filter)); + + if (!senderMatch && !subjectMatch) { + continue; + } + + let code = extractVerificationCode(meta.combinedText); + let opened = null; + + if (!code) { + opened = await openMailItemAndRead(item); + const openedSender = opened.sender.toLowerCase(); + const openedBody = opened.bodyText.toLowerCase(); + const openedSenderMatch = normalizedSenderFilters.some((filter) => openedSender.includes(filter)); + const openedSubjectMatch = normalizedSubjectFilters.some((filter) => openedBody.includes(filter)); + if (!openedSenderMatch && !openedSubjectMatch && !senderMatch && !subjectMatch) { + continue; + } + code = extractVerificationCode(opened.combinedText); + } + + if (!code) { + continue; + } + if (excludedCodeSet.has(code)) { + log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info'); + continue; + } + + const source = useFallback && existingSignatures.has(signature) ? '回退匹配邮件' : '新邮件'; + log(`步骤 ${step}:已找到验证码:${code}(来源:${source})`, 'ok'); + return { + ok: true, + code, + emailTimestamp: Date.now(), + preview: (opened?.combinedText || meta.combinedText).slice(0, 160), + }; + } + + if (attempt === FALLBACK_AFTER + 1) { + log(`步骤 ${step}:连续 ${FALLBACK_AFTER} 次未发现新邮件,开始回退到首封匹配邮件`, 'warn'); + } + + if (attempt < maxAttempts) { + await sleep(intervalMs); + } + } + + throw new Error( + `${Math.round((maxAttempts * intervalMs) / 1000)} 秒后仍未在 iCloud 邮箱中找到新的匹配邮件。请手动检查收件箱。` + ); + } +} diff --git a/content/utils.js b/content/utils.js index 9c30d4a..1960e26 100644 --- a/content/utils.js +++ b/content/utils.js @@ -10,6 +10,7 @@ const SCRIPT_SOURCE = (() => { if (hostname === 'mail.qq.com' || hostname === 'wx.mail.qq.com') return 'qq-mail'; if (hostname === 'mail.163.com' || hostname.endsWith('.mail.163.com') || hostname === 'webmail.vip.163.com') return 'mail-163'; if (hostname === 'mail.google.com') return 'gmail-mail'; + if (hostname === 'www.icloud.com' || hostname === 'www.icloud.com.cn') return 'icloud-mail'; if (url.includes('duckduckgo.com/email/settings/autofill')) return 'duck-mail'; if (url.includes('chatgpt.com')) return 'chatgpt'; if (url.includes("2925.com")) return "mail-2925"; diff --git a/icloud-utils.js b/icloud-utils.js index 82c7cc1..35f1f14 100644 --- a/icloud-utils.js +++ b/icloud-utils.js @@ -29,6 +29,13 @@ return ''; } + function getIcloudMailUrlForHost(host) { + const normalizedHost = normalizeIcloudHost(host); + if (normalizedHost === 'icloud.com') return 'https://www.icloud.com/mail/'; + if (normalizedHost === 'icloud.com.cn') return 'https://www.icloud.com.cn/mail/'; + return ''; + } + function getIcloudSetupUrlForHost(host) { const normalizedHost = normalizeIcloudHost(host); if (normalizedHost === 'icloud.com') return 'https://setup.icloud.com/setup/ws/1'; @@ -166,6 +173,7 @@ getConfiguredIcloudHostPreference, getIcloudHostHintFromMessage, getIcloudLoginUrlForHost, + getIcloudMailUrlForHost, getIcloudSetupUrlForHost, normalizeBooleanMap, normalizeIcloudAliasList, diff --git a/manifest.json b/manifest.json index ca213fb..4b3092f 100644 --- a/manifest.json +++ b/manifest.json @@ -75,6 +75,19 @@ "all_frames": true, "run_at": "document_idle" }, + { + "matches": [ + "https://www.icloud.com/*", + "https://www.icloud.com.cn/*" + ], + "js": [ + "content/activation-utils.js", + "content/utils.js", + "content/icloud-mail.js" + ], + "all_frames": true, + "run_at": "document_idle" + }, { "matches": [ "https://duckduckgo.com/email/settings/autofill*" diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 5caa3f4..ab169b8 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -162,6 +162,7 @@ + @@ -602,6 +603,7 @@
+ diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index b1d753f..c145bff 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -202,6 +202,7 @@ 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 ICLOUD_PROVIDER = 'icloud'; const GMAIL_PROVIDER = 'gmail'; const LUCKMAIL_PROVIDER = 'luckmail-api'; const DEFAULT_LUCKMAIL_BASE_URL = 'https://mails.luckyous.com'; @@ -258,6 +259,13 @@ const normalizeLuckmailTimestampValue = window.LuckMailUtils?.normalizeTimestamp 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 || '保留'; +const normalizeIcloudHost = window.IcloudUtils?.normalizeIcloudHost + || ((value) => { + const normalized = String(value || '').trim().toLowerCase(); + return normalized === 'icloud.com' || normalized === 'icloud.com.cn' ? normalized : ''; + }); +const getIcloudLoginUrlForHost = window.IcloudUtils?.getIcloudLoginUrlForHost + || ((host) => host === 'icloud.com.cn' ? 'https://www.icloud.com.cn/' : (host === 'icloud.com' ? 'https://www.icloud.com/' : '')); let lastRenderedLuckmailPurchases = []; let luckmailSelectedPurchaseIds = new Set(); @@ -267,6 +275,10 @@ let luckmailRefreshQueued = false; btnAutoCancelSchedule?.remove(); const MAIL_PROVIDER_LOGIN_CONFIGS = { + [ICLOUD_PROVIDER]: { + label: 'iCloud 邮箱', + buttonLabel: '登录', + }, [GMAIL_PROVIDER]: { label: 'Gmail 邮箱', url: 'https://mail.google.com/mail/u/0/#inbox', @@ -1468,7 +1480,7 @@ function applySettingsState(state) { inputSub2ApiPassword.value = state?.sub2apiPassword || ''; inputSub2ApiGroup.value = state?.sub2apiGroupName || ''; const restoredMailProvider = isCustomMailProvider(state?.mailProvider) - || ['hotmail-api', GMAIL_PROVIDER, 'luckmail-api', '163', '163-vip', 'qq', 'inbucket', '2925', 'cloudflare-temp-email'].includes(String(state?.mailProvider || '').trim()) + || [ICLOUD_PROVIDER, 'hotmail-api', GMAIL_PROVIDER, 'luckmail-api', '163', '163-vip', 'qq', 'inbucket', '2925', 'cloudflare-temp-email'].includes(String(state?.mailProvider || '').trim()) ? String(state?.mailProvider || '163').trim() : (String(state?.emailGenerator || '').trim().toLowerCase() === 'custom' || String(state?.emailGenerator || '').trim().toLowerCase() === 'manual' @@ -1787,6 +1799,10 @@ function isLuckmailProvider(provider = selectMailProvider.value) { return String(provider || '').trim().toLowerCase() === LUCKMAIL_PROVIDER; } +function isIcloudMailProvider(provider = selectMailProvider.value) { + return String(provider || '').trim().toLowerCase() === ICLOUD_PROVIDER; +} + function normalizeLuckmailBaseUrl(value = '') { const trimmed = String(value || '').trim(); if (!trimmed) { @@ -2313,8 +2329,17 @@ function getMailProviderLoginConfig(provider = selectMailProvider.value) { return MAIL_PROVIDER_LOGIN_CONFIGS[String(provider || '').trim()] || null; } +function getSelectedIcloudHostPreference() { + return normalizeIcloudHost(selectIcloudHostPreference?.value || latestState?.icloudHostPreference || '') + || normalizeIcloudHost(latestState?.preferredIcloudHost) + || 'icloud.com'; +} + function getMailProviderLoginUrl(provider = selectMailProvider.value) { const config = getMailProviderLoginConfig(provider); + if (String(provider || '').trim() === ICLOUD_PROVIDER) { + return getIcloudLoginUrlForHost(getSelectedIcloudHostPreference()); + } const url = String(config?.url || '').trim(); return url ? url : ''; } @@ -2614,6 +2639,7 @@ function updateMailProviderUI() { const useHotmail = selectMailProvider.value === 'hotmail-api'; const useLuckmail = isLuckmailProvider(); const useCustomEmail = isCustomMailProvider(); + const useIcloudProvider = isIcloudMailProvider(); const useEmailGenerator = !useHotmail && !useLuckmail && !useGeneratedAlias && !useCustomEmail; const useCloudflareTempEmailProvider = selectMailProvider.value === 'cloudflare-temp-email'; updateMailLoginButtonState(); @@ -2635,7 +2661,7 @@ function updateMailProviderUI() { rowEmailGenerator.style.display = useEmailGenerator ? '' : 'none'; } if (icloudSection) { - const showIcloudSection = useEmailGenerator && useIcloud; + const showIcloudSection = (useEmailGenerator && useIcloud) || useIcloudProvider; icloudSection.style.display = showIcloudSection ? '' : 'none'; if (showIcloudSection && !lastRenderedIcloudAliases.length) { queueIcloudAliasRefresh(); diff --git a/tests/background-icloud-mail-provider.test.js b/tests/background-icloud-mail-provider.test.js new file mode 100644 index 0000000..42e6d3b --- /dev/null +++ b/tests/background-icloud-mail-provider.test.js @@ -0,0 +1,145 @@ +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('normalizeMailProvider keeps icloud provider', () => { + const bundle = extractFunction('normalizeMailProvider'); + const api = new Function(` +const ICLOUD_PROVIDER = 'icloud'; +const GMAIL_PROVIDER = 'gmail'; +const HOTMAIL_PROVIDER = 'hotmail-api'; +const LUCKMAIL_PROVIDER = 'luckmail-api'; +const CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email'; +const PERSISTED_SETTING_DEFAULTS = { mailProvider: '163' }; +${bundle} +return { normalizeMailProvider }; +`)(); + + assert.equal(api.normalizeMailProvider('icloud'), 'icloud'); + assert.equal(api.normalizeMailProvider('ICLOUD'), 'icloud'); +}); + +test('getMailConfig returns icloud mail tab config with host preference', () => { + const bundle = extractFunction('getMailConfig'); + const api = new Function(` +const ICLOUD_PROVIDER = 'icloud'; +const GMAIL_PROVIDER = 'gmail'; +const HOTMAIL_PROVIDER = 'hotmail-api'; +const LUCKMAIL_PROVIDER = 'luckmail-api'; +const CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email'; +function normalizeIcloudHost(value = '') { + const normalized = String(value || '').trim().toLowerCase(); + return normalized === 'icloud.com' || normalized === 'icloud.com.cn' ? normalized : ''; +} +function normalizeInbucketOrigin(value) { return String(value || '').trim(); } +function getConfiguredIcloudHostPreference(state) { + const normalized = String(state?.icloudHostPreference || '').trim().toLowerCase(); + return normalized === 'icloud.com' || normalized === 'icloud.com.cn' ? normalized : ''; +} +function getIcloudLoginUrlForHost(host) { + return host === 'icloud.com.cn' ? 'https://www.icloud.com.cn/' : 'https://www.icloud.com/'; +} +function getIcloudMailUrlForHost(host) { + return host === 'icloud.com.cn' ? 'https://www.icloud.com.cn/mail/' : 'https://www.icloud.com/mail/'; +} +${bundle} +return { getMailConfig }; +`)(); + + assert.deepEqual(api.getMailConfig({ + mailProvider: 'icloud', + icloudHostPreference: 'icloud.com.cn', + }), { + source: 'icloud-mail', + url: 'https://www.icloud.com.cn/mail/', + label: 'iCloud 邮箱', + navigateOnReuse: true, + }); +}); + +test('getMailConfig reuses preferred icloud host when preference is auto', () => { + const bundle = extractFunction('getMailConfig'); + const api = new Function(` +const ICLOUD_PROVIDER = 'icloud'; +const GMAIL_PROVIDER = 'gmail'; +const HOTMAIL_PROVIDER = 'hotmail-api'; +const LUCKMAIL_PROVIDER = 'luckmail-api'; +const CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email'; +function normalizeIcloudHost(value = '') { + const normalized = String(value || '').trim().toLowerCase(); + return normalized === 'icloud.com' || normalized === 'icloud.com.cn' ? normalized : ''; +} +function normalizeInbucketOrigin(value) { return String(value || '').trim(); } +function getConfiguredIcloudHostPreference() { + return ''; +} +function getIcloudLoginUrlForHost(host) { + return host === 'icloud.com.cn' ? 'https://www.icloud.com.cn/' : 'https://www.icloud.com/'; +} +function getIcloudMailUrlForHost(host) { + return host === 'icloud.com.cn' ? 'https://www.icloud.com.cn/mail/' : 'https://www.icloud.com/mail/'; +} +${bundle} +return { getMailConfig }; +`)(); + + assert.deepEqual(api.getMailConfig({ + mailProvider: 'icloud', + icloudHostPreference: 'auto', + preferredIcloudHost: 'icloud.com.cn', + }), { + source: 'icloud-mail', + url: 'https://www.icloud.com.cn/mail/', + label: 'iCloud 邮箱', + navigateOnReuse: true, + }); +}); diff --git a/tests/icloud-mail-content.test.js b/tests/icloud-mail-content.test.js new file mode 100644 index 0000000..42a7cac --- /dev/null +++ b/tests/icloud-mail-content.test.js @@ -0,0 +1,181 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); + +const source = fs.readFileSync('content/icloud-mail.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('readOpenedMailBody falls back to thread detail pane and extracts verification code', () => { + const bundle = [ + extractFunction('normalizeText'), + extractFunction('extractVerificationCode'), + extractFunction('getOpenedMailBodyRoot'), + extractFunction('readOpenedMailBody'), + ].join('\n'); + + const api = new Function(` +const detailPane = { + innerText: '此邮件包含远程内容。 你的 ChatGPT 代码为 731091 输入此临时验证码以继续:731091', + textContent: '此邮件包含远程内容。 你的 ChatGPT 代码为 731091 输入此临时验证码以继续:731091', +}; +const document = { + querySelector(selector) { + if (selector.includes('.pane.thread-detail-pane')) { + return detailPane; + } + return null; + }, +}; +${bundle} +return { readOpenedMailBody, extractVerificationCode }; +`)(); + + const bodyText = api.readOpenedMailBody(); + assert.match(bodyText, /731091/); + assert.equal(api.extractVerificationCode(bodyText), '731091'); +}); + +test('readOpenedMailBody ignores conversation list rows when no detail pane is open', () => { + const bundle = [ + extractFunction('normalizeText'), + extractFunction('getOpenedMailBodyRoot'), + extractFunction('readOpenedMailBody'), + ].join('\n'); + + const api = new Function(` +const document = { + querySelector(selector) { + if (selector === '.mail-message-defaults, .pane.thread-detail-pane') { + return null; + } + throw new Error('unexpected selector: ' + selector); + }, +}; +${bundle} +return { readOpenedMailBody }; +`)(); + + assert.equal(api.readOpenedMailBody(), ''); +}); + +test('isThreadItemSelected follows the selected thread-list-item instead of the content container itself', () => { + const bundle = [ + extractFunction('normalizeText'), + extractFunction('getThreadItemMetadata'), + extractFunction('buildItemSignature'), + extractFunction('getThreadListItemRoot'), + extractFunction('isThreadItemSelected'), + ].join('\n'); + + const selectedRoot = { + getAttribute(name) { + return name === 'aria-selected' ? 'true' : ''; + }, + className: 'thread-list-item ic-z3c00x', + }; + const selectedItem = { + closest() { + return selectedRoot; + }, + getAttribute() { + return ''; + }, + querySelector(selector) { + const map = { + '.thread-participants': { textContent: 'OpenAI' }, + '.thread-subject': { textContent: '你的 ChatGPT 代码为 731091' }, + '.thread-preview': { textContent: '输入此临时验证码以继续:731091' }, + '.thread-timestamp': { textContent: '下午1:35' }, + }; + return map[selector] || null; + }, + }; + const staleRoot = { + getAttribute() { + return ''; + }, + className: 'thread-list-item ic-z3c00x', + }; + const staleItem = { + closest() { + return staleRoot; + }, + getAttribute() { + return ''; + }, + querySelector(selector) { + const map = { + '.thread-participants': { textContent: 'JetBrains Sales' }, + '.thread-subject': { textContent: '旧邮件' }, + '.thread-preview': { textContent: '旧摘要' }, + '.thread-timestamp': { textContent: '2026/3/4' }, + }; + return map[selector] || null; + }, + }; + + const api = new Function(` +let threadItems = []; +function collectThreadItems() { + return threadItems; +} +${bundle} +return { + buildItemSignature, + isThreadItemSelected, + setThreadItems(next) { + threadItems = next; + }, +}; +`)(); + + api.setThreadItems([selectedItem, staleItem]); + assert.equal(api.isThreadItemSelected(staleItem, api.buildItemSignature(selectedItem)), true); + assert.equal(api.isThreadItemSelected(staleItem, api.buildItemSignature(staleItem)), false); +}); diff --git a/tests/icloud-utils.test.js b/tests/icloud-utils.test.js index 5d8fa68..8c745b8 100644 --- a/tests/icloud-utils.test.js +++ b/tests/icloud-utils.test.js @@ -7,6 +7,7 @@ const { getConfiguredIcloudHostPreference, getIcloudHostHintFromMessage, getIcloudLoginUrlForHost, + getIcloudMailUrlForHost, getIcloudSetupUrlForHost, normalizeBooleanMap, normalizeIcloudAliasList, @@ -24,6 +25,7 @@ test('normalizeIcloudHost and host preference helpers resolve supported hosts', assert.equal(getConfiguredIcloudHostPreference({ icloudHostPreference: 'icloud.com' }), 'icloud.com'); assert.equal(getConfiguredIcloudHostPreference({ icloudHostPreference: 'auto' }), ''); assert.equal(getIcloudLoginUrlForHost('icloud.com.cn'), 'https://www.icloud.com.cn/'); + assert.equal(getIcloudMailUrlForHost('icloud.com'), 'https://www.icloud.com/mail/'); assert.equal(getIcloudSetupUrlForHost('icloud.com'), 'https://setup.icloud.com/setup/ws/1'); }); diff --git a/tests/sidepanel-icloud-provider.test.js b/tests/sidepanel-icloud-provider.test.js new file mode 100644 index 0000000..58808e9 --- /dev/null +++ b/tests/sidepanel-icloud-provider.test.js @@ -0,0 +1,81 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); + +const source = fs.readFileSync('sidepanel/sidepanel.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('getMailProviderLoginUrl reuses preferred icloud host when preference is auto', () => { + const bundle = [ + extractFunction('getSelectedIcloudHostPreference'), + extractFunction('getMailProviderLoginUrl'), + ].join('\n'); + + const api = new Function(` +const ICLOUD_PROVIDER = 'icloud'; +const selectMailProvider = { value: ICLOUD_PROVIDER }; +const selectIcloudHostPreference = { value: 'auto' }; +const latestState = { icloudHostPreference: 'auto', preferredIcloudHost: 'icloud.com.cn' }; +function normalizeIcloudHost(value = '') { + const normalized = String(value || '').trim().toLowerCase(); + return normalized === 'icloud.com' || normalized === 'icloud.com.cn' ? normalized : ''; +} +function getIcloudLoginUrlForHost(host) { + return host === 'icloud.com.cn' ? 'https://www.icloud.com.cn/' : 'https://www.icloud.com/'; +} +function getMailProviderLoginConfig() { + return { label: 'iCloud 邮箱' }; +} +${bundle} +return { getSelectedIcloudHostPreference, getMailProviderLoginUrl }; +`)(); + + assert.equal(api.getSelectedIcloudHostPreference(), 'icloud.com.cn'); + assert.equal(api.getMailProviderLoginUrl(), 'https://www.icloud.com.cn/'); +});