From 34ee5a326333005181d83a317e9b7fdb4e97589f Mon Sep 17 00:00:00 2001 From: Hero Date: Tue, 14 Apr 2026 19:52:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8E=A5=E5=85=A5hotmail=20api?= =?UTF-8?q?=E5=AF=B9=E6=8E=A5=E4=B8=8E=E8=B7=A8=E5=9F=9F=E8=A7=84=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background.js | 226 +++++++++++--------------- hotmail-utils.js | 9 ++ manifest.json | 3 +- microsoft-email.js | 251 +++++++++++++++++++++++++++++ sidepanel/sidepanel.html | 8 +- sidepanel/sidepanel.js | 15 +- tests/hotmail-api-mode.test.js | 31 ++++ tests/hotmail-cors-headers.test.js | 23 +++ tests/hotmail-utils.test.js | 9 ++ tests/microsoft-email.test.js | 147 +++++++++++++++++ 10 files changed, 582 insertions(+), 140 deletions(-) create mode 100644 microsoft-email.js create mode 100644 tests/hotmail-api-mode.test.js create mode 100644 tests/hotmail-cors-headers.test.js create mode 100644 tests/microsoft-email.test.js diff --git a/background.js b/background.js index 11fd5ec..de90716 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', 'microsoft-email.js', 'content/activation-utils.js'); const { buildHotmailMailApiLatestUrl, @@ -10,6 +10,7 @@ const { getHotmailMailApiRequestConfig, getHotmailVerificationPollConfig, getHotmailVerificationRequestTimestamp, + normalizeHotmailServiceMode, normalizeHotmailMailApiMessages, pickHotmailAccountForRun, pickVerificationMessage, @@ -17,6 +18,10 @@ const { pickVerificationMessageWithTimeFallback, shouldClearHotmailCurrentSelection, } = self.HotmailUtils; +const { + fetchMicrosoftMailboxMessages, + fetchMicrosoftVerificationCode, +} = self.MultiPageMicrosoftEmail; const { isRecoverableStep9AuthFailure, } = self.MultiPageActivationUtils; @@ -49,8 +54,36 @@ const DEFAULT_HOTMAIL_REMOTE_BASE_URL = ''; const DEFAULT_HOTMAIL_LOCAL_BASE_URL = 'http://127.0.0.1:17373'; const HOTMAIL_LOCAL_HELPER_TIMEOUT_MS = 45000; const DISPLAY_TIMEZONE = 'Asia/Shanghai'; +const MICROSOFT_TOKEN_DNR_RULE_ID = 1001; initializeSessionStorageAccess(); +setupDeclarativeNetRequestRules(); + +function setupDeclarativeNetRequestRules() { + if (!chrome.declarativeNetRequest?.updateDynamicRules) { + return; + } + + chrome.declarativeNetRequest.updateDynamicRules({ + removeRuleIds: [MICROSOFT_TOKEN_DNR_RULE_ID], + addRules: [{ + id: MICROSOFT_TOKEN_DNR_RULE_ID, + priority: 1, + action: { + type: 'modifyHeaders', + requestHeaders: [ + { header: 'Origin', operation: 'remove' }, + ], + }, + condition: { + urlFilter: 'login.microsoftonline.com/*/oauth2/v2.0/token', + resourceTypes: ['xmlhttprequest'], + }, + }], + }).catch((error) => { + console.warn(LOG_PREFIX, 'Failed to setup declarativeNetRequest rules:', error?.message || error); + }); +} // ============================================================ // 状态管理(chrome.storage.session + chrome.storage.local) @@ -274,10 +307,6 @@ function normalizeCloudflareDomains(values) { return normalizedDomains; } -function normalizeHotmailServiceMode(rawValue = '') { - return HOTMAIL_SERVICE_MODE_LOCAL; -} - function normalizeHotmailRemoteBaseUrl(rawValue = '') { const value = String(rawValue || '').trim(); if (!value) return DEFAULT_HOTMAIL_REMOTE_BASE_URL; @@ -831,55 +860,37 @@ async function requestHotmailRemoteMailbox(account, mailbox = 'INBOX') { throw new Error(`Hotmail 账号 ${account.email || account.id} 缺少刷新令牌(refresh token)。`); } - const serviceSettings = getHotmailServiceSettings(await getState()); - const url = buildHotmailMailApiLatestUrl({ - apiUrl: buildHotmailRemoteEndpoint(serviceSettings.remoteBaseUrl, '/api/mail-new'), - clientId: account.clientId, - email: account.email, - refreshToken: account.refreshToken, - mailbox, - responseType: 'json', - }); const { timeoutMs } = getHotmailMailApiRequestConfig(); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(new Error('timeout')), timeoutMs); - let response; try { - response = await fetch(url, { method: 'GET', signal: controller.signal }); + const result = await fetchMicrosoftMailboxMessages({ + clientId: account.clientId, + refreshToken: account.refreshToken, + top: 10, + signal: controller.signal, + }); + + return { + mailbox, + payload: { + source: 'microsoft-api', + }, + messages: normalizeHotmailMailApiMessages(result.messages).map((message) => ({ + ...message, + mailbox: 'INBOX', + })), + nextRefreshToken: result.nextRefreshToken, + }; } catch (err) { if (err?.name === 'AbortError') { - throw new Error(`Hotmail API 请求超时(>${Math.round(timeoutMs / 1000)} 秒):${mailbox}`); + throw new Error(`Hotmail API 对接请求超时(>${Math.round(timeoutMs / 1000)} 秒):${mailbox}`); } - throw new Error(`Hotmail API 请求失败:${err.message}`); + throw new Error(`Hotmail API 对接请求失败:${err.message}`); } finally { clearTimeout(timeoutId); } - - const text = await response.text(); - let payload = {}; - try { - payload = text ? JSON.parse(text) : {}; - } catch { - payload = { raw: text }; - } - - if (!response.ok) { - const errorText = payload?.message || payload?.error || payload?.msg || text || `HTTP ${response.status}`; - throw new Error(`Hotmail API 请求失败:${errorText}`); - } - - if (payload && payload.success === false) { - const errorText = payload?.message || payload?.msg || payload?.error || '未知错误'; - throw new Error(`Hotmail API 返回失败:${errorText}`); - } - - return { - mailbox, - payload, - messages: normalizeHotmailMailApiMessages(payload?.data), - nextRefreshToken: String(payload?.new_refresh_token || payload?.newRefreshToken || '').trim(), - }; } function applyHotmailApiResultToAccount(account, apiResult) { @@ -903,30 +914,28 @@ function buildHotmailMailApiFailureAccount(account, errorMessage) { async function fetchHotmailMailboxMessagesFromRemoteService(account, mailboxes = HOTMAIL_MAILBOXES) { let workingAccount = normalizeHotmailAccount(account); - const mailboxResults = []; + const requestedMailboxes = Array.isArray(mailboxes) && mailboxes.length ? mailboxes : ['INBOX']; try { - for (const mailbox of mailboxes) { - const result = await requestHotmailRemoteMailbox(workingAccount, mailbox); - workingAccount = applyHotmailApiResultToAccount(workingAccount, result); - mailboxResults.push({ - mailbox, - count: result.messages.length, - messages: result.messages.map((message) => ({ ...message, mailbox })), - }); - } + const result = await requestHotmailRemoteMailbox(workingAccount, 'INBOX'); + workingAccount = applyHotmailApiResultToAccount(workingAccount, result); + const inboxMessages = result.messages.map((message) => ({ ...message, mailbox: 'INBOX' })); + const mailboxResults = requestedMailboxes.map((mailbox) => ({ + mailbox, + count: mailbox === 'INBOX' ? inboxMessages.length : 0, + messages: mailbox === 'INBOX' ? inboxMessages : [], + })); + const savedAccount = await upsertHotmailAccount(workingAccount); + return { + account: savedAccount, + mailboxResults, + messages: mailboxResults.flatMap((item) => item.messages), + }; } catch (err) { const failedAccount = buildHotmailMailApiFailureAccount(workingAccount, err.message); await upsertHotmailAccount(failedAccount); throw err; } - - const savedAccount = await upsertHotmailAccount(workingAccount); - return { - account: savedAccount, - mailboxResults, - messages: mailboxResults.flatMap((item) => item.messages), - }; } async function requestHotmailLocalMessages(account, mailboxes = HOTMAIL_MAILBOXES) { @@ -1195,76 +1204,33 @@ async function pollHotmailVerificationCode(step, state, pollPayload = {}) { if (serviceSettings.mode === HOTMAIL_SERVICE_MODE_LOCAL) { return pollHotmailVerificationCodeViaLocalHelper(step, account, pollPayload); } - - const maxAttempts = Number(pollPayload.maxAttempts) || 5; - const intervalMs = Number(pollPayload.intervalMs) || 3000; - let lastError = null; - - function summarizeMessagesForLog(messages) { - return (messages || []) - .slice() - .sort((left, right) => { - const leftTime = Date.parse(left.receivedDateTime || '') || 0; - const rightTime = Date.parse(right.receivedDateTime || '') || 0; - return rightTime - leftTime; - }) - .slice(0, 3) - .map((message) => { - const receivedAt = message?.receivedDateTime || '未知时间'; - const sender = message?.from?.emailAddress?.address || '未知发件人'; - const subject = message?.subject || '(无主题)'; - const preview = String(message?.bodyPreview || '').replace(/\s+/g, ' ').trim().slice(0, 80); - return `[${message.mailbox || 'INBOX'}] ${receivedAt} | ${sender} | ${subject} | ${preview}`; - }) - .join(' || '); + try { + await addLog(`步骤 ${step}:正在通过 API对接 轮询 Hotmail 验证码...`, 'info'); + const result = await fetchMicrosoftVerificationCode({ + token: account.refreshToken, + clientId: account.clientId, + filterAfterTimestamp: pollPayload.filterAfterTimestamp || 0, + maxRetries: Number(pollPayload.maxAttempts) || 5, + retryDelayMs: Number(pollPayload.intervalMs) || 3000, + log: (message) => { + void addLog(`步骤 ${step}:${message}`, 'info'); + }, + }); + account = await upsertHotmailAccount(applyHotmailApiResultToAccount(account, { + nextRefreshToken: result.nextRefreshToken, + })); + await addLog(`步骤 ${step}:已通过 API对接 找到验证码:${result.code}`, 'ok'); + return { + ok: true, + code: result.code, + emailTimestamp: result.emailTimestamp || Date.now(), + mailId: result.messageId || '', + }; + } catch (err) { + const failedAccount = buildHotmailMailApiFailureAccount(account, err.message); + await upsertHotmailAccount(failedAccount); + throw new Error(`步骤 ${step}:${err.message}`); } - - for (let attempt = 1; attempt <= maxAttempts; attempt++) { - throwIfStopped(); - try { - await addLog(`步骤 ${step}:正在轮询 Hotmail 邮件(${attempt}/${maxAttempts})...`, 'info'); - const fetchResult = await fetchHotmailMailboxMessages(account, HOTMAIL_MAILBOXES); - account = fetchResult.account; - const matchResult = pickVerificationMessageWithTimeFallback(fetchResult.messages, { - afterTimestamp: pollPayload.filterAfterTimestamp || 0, - senderFilters: pollPayload.senderFilters || [], - subjectFilters: pollPayload.subjectFilters || [], - excludeCodes: pollPayload.excludeCodes || [], - }); - const match = matchResult.match; - - if (match?.code) { - const mailboxLabel = match.message?.mailbox || 'INBOX'; - if (matchResult.usedRelaxedFilters) { - const fallbackLabel = matchResult.usedTimeFallback ? '宽松匹配 + 时间回退' : '宽松匹配'; - await addLog(`步骤 ${step}:严格规则未命中,已改用 ${fallbackLabel} 并命中 Hotmail ${mailboxLabel} 验证码。`, 'warn'); - } - await addLog(`步骤 ${step}:已在 Hotmail ${mailboxLabel} 中找到验证码:${match.code}`, 'ok'); - return { - ok: true, - code: match.code, - emailTimestamp: match.receivedAt || Date.now(), - mailId: match.message?.id || '', - }; - } - - lastError = new Error(`步骤 ${step}:暂未在 Hotmail 收件箱中找到匹配验证码(${attempt}/${maxAttempts})。`); - await addLog(lastError.message, attempt === maxAttempts ? 'warn' : 'info'); - const mailSummary = summarizeMessagesForLog(fetchResult.messages); - if (mailSummary) { - await addLog(`步骤 ${step}:最近邮件样本:${mailSummary}`, 'info'); - } - } catch (err) { - lastError = err; - await addLog(`步骤 ${step}:Hotmail 收件箱轮询失败:${err.message}`, 'warn'); - } - - if (attempt < maxAttempts) { - await sleepWithStop(intervalMs); - } - } - - throw lastError || new Error(`步骤 ${step}:未在 Hotmail 收件箱中找到新的匹配验证码。`); } function generateRandomSuffix(length = 6) { @@ -4730,7 +4696,7 @@ function getMailConfig(state) { return { provider: 'custom', label: '自定义邮箱' }; } if (provider === HOTMAIL_PROVIDER) { - return { provider: HOTMAIL_PROVIDER, label: 'Hotmail(远程/本地)' }; + return { provider: HOTMAIL_PROVIDER, label: 'Hotmail(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 邮箱' }; diff --git a/hotmail-utils.js b/hotmail-utils.js index 97e07bf..5d736ca 100644 --- a/hotmail-utils.js +++ b/hotmail-utils.js @@ -7,6 +7,8 @@ root.HotmailUtils = factory(); })(typeof self !== 'undefined' ? self : globalThis, function createHotmailUtils() { const HOTMAIL_MAIL_API_URL = 'https://apple.882263.xyz/api/mail-new'; + const HOTMAIL_SERVICE_MODE_REMOTE = 'remote'; + const HOTMAIL_SERVICE_MODE_LOCAL = 'local'; function normalizeText(value) { return String(value || '') @@ -25,6 +27,12 @@ return Number.isFinite(timestamp) ? timestamp : 0; } + function normalizeHotmailServiceMode(rawValue = '') { + return String(rawValue || '').trim().toLowerCase() === HOTMAIL_SERVICE_MODE_REMOTE + ? HOTMAIL_SERVICE_MODE_REMOTE + : HOTMAIL_SERVICE_MODE_LOCAL; + } + function extractVerificationCode(text) { const source = String(text || ''); const matchCn = source.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/i); @@ -368,6 +376,7 @@ getHotmailVerificationPollConfig, getHotmailVerificationRequestTimestamp, isAuthorizedHotmailAccount, + normalizeHotmailServiceMode, normalizeHotmailMailApiMessages, normalizeTimestamp, parseHotmailImportText, diff --git a/manifest.json b/manifest.json index cca9009..f0196b9 100644 --- a/manifest.json +++ b/manifest.json @@ -1,13 +1,14 @@ { "manifest_version": 3, "name": "多页面自动化", - "version": "7.8.0", + "version": "8.0.0", "description": "用于自动执行多步骤 OAuth 注册流程", "permissions": [ "sidePanel", "alarms", "tabs", "webNavigation", + "declarativeNetRequest", "debugger", "storage", "scripting", diff --git a/microsoft-email.js b/microsoft-email.js new file mode 100644 index 0000000..f4fdcec --- /dev/null +++ b/microsoft-email.js @@ -0,0 +1,251 @@ +(function attachMicrosoftEmailHelpers(globalScope) { + const OPENAI_SENDER_PATTERNS = [ + /openai\.com/i, + /auth0\.openai\.com/i, + ]; + const CODE_PATTERN = /\b(\d{6})\b/; + const TOKEN_ENDPOINT = 'https://login.microsoftonline.com/consumers/oauth2/v2.0/token'; + const OUTLOOK_API_BASE = 'https://outlook.office.com/api/v2.0/me/messages'; + + async function exchangeRefreshToken(clientId, refreshToken, options = {}) { + const fetchImpl = options.fetchImpl || globalScope.fetch; + if (typeof fetchImpl !== 'function') { + throw new Error('Microsoft 邮箱 helper 缺少 fetch 实现。'); + } + + const body = new URLSearchParams({ + client_id: clientId, + grant_type: 'refresh_token', + refresh_token: refreshToken, + }); + const response = await fetchImpl(TOKEN_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: body.toString(), + signal: options.signal, + }); + + if (!response.ok) { + const text = await response.text().catch(() => ''); + const errorInfo = (() => { + try { + return JSON.parse(text); + } catch { + return {}; + } + })(); + throw new Error( + errorInfo.error_description + || `Token exchange failed (${response.status}): ${text.slice(0, 200)}` + ); + } + + const data = await response.json(); + if (!data.access_token) { + throw new Error('Token exchange response missing access_token.'); + } + return data; + } + + async function fetchOutlookMessages(accessToken, options = {}) { + const { top = 5, signal } = options; + const fetchImpl = options.fetchImpl || globalScope.fetch; + if (typeof fetchImpl !== 'function') { + throw new Error('Microsoft 邮箱 helper 缺少 fetch 实现。'); + } + + const response = await fetchImpl( + `${OUTLOOK_API_BASE}?$top=${encodeURIComponent(top)}&$orderby=ReceivedDateTime desc&$select=From,Subject,ReceivedDateTime,BodyPreview,Body`, + { + method: 'GET', + headers: { Authorization: `Bearer ${accessToken}` }, + signal, + } + ); + + if (response.status === 401 || response.status === 403) { + throw new Error('Microsoft Graph token invalid or expired.'); + } + + if (!response.ok) { + const body = await response.text().catch(() => ''); + throw new Error(`Outlook API request failed (${response.status}): ${body || response.statusText}`); + } + + const payload = await response.json(); + return Array.isArray(payload?.value) ? payload.value : []; + } + + function normalizeMessage(message) { + return { + from: { + emailAddress: { + address: message?.From?.EmailAddress?.Address + || message?.from?.emailAddress?.address + || '', + }, + }, + subject: message?.Subject || message?.subject || '', + receivedDateTime: message?.ReceivedDateTime || message?.receivedDateTime || '', + bodyPreview: message?.BodyPreview || message?.bodyPreview || '', + body: { + content: message?.Body?.Content || message?.body?.content || '', + }, + id: message?.Id || message?.id || '', + }; + } + + function getMessageSender(message) { + return String( + message?.from?.emailAddress?.address + || message?.sender?.emailAddress?.address + || '' + ).trim(); + } + + function getMessageTimestamp(message) { + const value = Date.parse(message?.receivedDateTime || message?.createdDateTime || ''); + return Number.isFinite(value) ? value : 0; + } + + function getMessageSearchText(message) { + return [ + message?.subject, + message?.bodyPreview, + message?.body?.content, + getMessageSender(message), + ] + .map((value) => String(value || '')) + .join('\n'); + } + + function isOpenAiMessage(message) { + const sender = getMessageSender(message); + if (OPENAI_SENDER_PATTERNS.some((pattern) => pattern.test(sender))) { + return true; + } + + const searchText = getMessageSearchText(message); + return OPENAI_SENDER_PATTERNS.some((pattern) => pattern.test(searchText)); + } + + function extractVerificationCodeFromMessages(messages, options = {}) { + const { filterAfterTimestamp = 0 } = options; + + for (const raw of messages) { + const message = normalizeMessage(raw); + const receivedAt = getMessageTimestamp(message); + if (receivedAt && receivedAt < Number(filterAfterTimestamp || 0)) { + continue; + } + if (!isOpenAiMessage(message)) { + continue; + } + + const match = getMessageSearchText(message).match(CODE_PATTERN); + if (!match) { + continue; + } + + return { + code: match[1], + emailTimestamp: receivedAt || Date.now(), + messageId: message?.id || null, + sender: getMessageSender(message), + subject: String(message?.subject || ''), + }; + } + + return null; + } + + async function fetchMicrosoftMailboxMessages(options = {}) { + const { + clientId, + refreshToken, + top = 5, + fetchImpl, + signal, + } = options; + + if (!refreshToken) { + throw new Error('Microsoft refresh token is empty.'); + } + if (!clientId) { + throw new Error('Microsoft client_id is empty.'); + } + + const tokenData = await exchangeRefreshToken(clientId, refreshToken, { fetchImpl, signal }); + const rawMessages = await fetchOutlookMessages(tokenData.access_token, { top, signal, fetchImpl }); + + return { + tokenData, + nextRefreshToken: String(tokenData?.refresh_token || '').trim(), + messages: rawMessages.map((message) => normalizeMessage(message)), + }; + } + + async function fetchMicrosoftVerificationCode(options = {}) { + const { + token, + clientId, + maxRetries = 3, + retryDelayMs = 10000, + log = null, + filterAfterTimestamp = 0, + fetchImpl, + signal, + } = options; + + if (!token) { + throw new Error('Microsoft refresh token is empty.'); + } + if (!clientId) { + throw new Error('Microsoft client_id is empty.'); + } + + const tokenData = await exchangeRefreshToken(clientId, token, { fetchImpl, signal }); + const accessToken = tokenData.access_token; + const nextRefreshToken = String(tokenData?.refresh_token || '').trim(); + + for (let attempt = 1; attempt <= maxRetries; attempt += 1) { + const messages = await fetchOutlookMessages(accessToken, { top: 5, signal, fetchImpl }); + const result = extractVerificationCodeFromMessages(messages, { filterAfterTimestamp }); + if (result) { + return { + ...result, + nextRefreshToken, + messages: messages.map((message) => normalizeMessage(message)), + }; + } + + if (attempt < maxRetries) { + if (typeof log === 'function') { + log(`Outlook API: attempt ${attempt}/${maxRetries} found no OpenAI verification mail, retrying...`); + } + await new Promise((resolve) => setTimeout(resolve, retryDelayMs)); + } + } + + throw new Error('No matching OpenAI verification email found.'); + } + + const api = { + CODE_PATTERN, + exchangeRefreshToken, + extractVerificationCodeFromMessages, + fetchMicrosoftMailboxMessages, + fetchMicrosoftVerificationCode, + fetchOutlookMessages, + getMessageSender, + getMessageTimestamp, + isOpenAiMessage, + normalizeMessage, + }; + + globalScope.MultiPageMicrosoftEmail = api; + + if (typeof module !== 'undefined' && module.exports) { + module.exports = api; + } +})(typeof globalThis !== 'undefined' ? globalThis : this); diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 944b7e7..ba1ec7b 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -153,7 +153,7 @@
+ API对接 +