feat: 接入hotmail api对接与跨域规则
This commit is contained in:
+96
-130
@@ -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 邮箱' };
|
||||
|
||||
@@ -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,
|
||||
|
||||
+2
-1
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
@@ -153,7 +153,7 @@
|
||||
<div class="data-inline">
|
||||
<select id="select-mail-provider" class="data-select">
|
||||
<option value="custom">自定义邮箱</option>
|
||||
<option value="hotmail-api">Hotmail(本地助手)</option>
|
||||
<option value="hotmail-api">Hotmail(账号池)</option>
|
||||
<option value="163">163 邮箱 (mail.163.com)</option>
|
||||
<option value="163-vip">163 VIP 邮箱 (vip.163.com)</option>
|
||||
<option value="qq">QQ 邮箱 (wx.mail.qq.com)</option>
|
||||
@@ -271,13 +271,13 @@
|
||||
<div class="data-row" id="row-hotmail-service-mode">
|
||||
<span class="data-label">接码模式</span>
|
||||
<div id="hotmail-service-mode-group" class="choice-group" role="group" aria-label="Hotmail 接码模式">
|
||||
<button type="button" class="choice-btn" data-hotmail-service-mode="remote" disabled title="远程服务暂时禁用">远程服务</button>
|
||||
<button type="button" class="choice-btn" data-hotmail-service-mode="remote">API对接</button>
|
||||
<button type="button" class="choice-btn" data-hotmail-service-mode="local">本地助手</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-hotmail-remote-base-url">
|
||||
<span class="data-label">远程服务</span>
|
||||
<input type="text" id="input-hotmail-remote-base-url" class="data-input mono" placeholder="请输入远程服务地址" />
|
||||
<span class="data-label">API对接</span>
|
||||
<input type="text" id="input-hotmail-remote-base-url" class="data-input mono" placeholder="微软邮箱 API 对接模式无需填写地址" />
|
||||
</div>
|
||||
<div class="data-row" id="row-hotmail-local-base-url" style="display:none;">
|
||||
<span class="data-label">本地助手</span>
|
||||
|
||||
+10
-5
@@ -177,6 +177,7 @@ const EYE_OPEN_ICON = '<svg width="14" height="14" viewBox="0 0 24 24" fill="non
|
||||
const EYE_CLOSED_ICON = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17.94 17.94A10.94 10.94 0 0 1 12 19C5 19 1 12 1 12a21.77 21.77 0 0 1 5.06-6.94"/><path d="M9.9 4.24A10.94 10.94 0 0 1 12 5c7 0 11 7 11 7a21.86 21.86 0 0 1-2.16 3.19"/><path d="M1 1l22 22"/><path d="M14.12 14.12a3 3 0 1 1-4.24-4.24"/></svg>';
|
||||
const COPY_ICON = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
||||
const parseHotmailImportText = window.HotmailUtils?.parseHotmailImportText;
|
||||
const normalizeHotmailServiceModeFromUtils = window.HotmailUtils?.normalizeHotmailServiceMode;
|
||||
const shouldClearHotmailCurrentSelection = window.HotmailUtils?.shouldClearHotmailCurrentSelection;
|
||||
const upsertHotmailAccountInList = window.HotmailUtils?.upsertHotmailAccountInList;
|
||||
const filterHotmailAccountsByUsage = window.HotmailUtils?.filterHotmailAccountsByUsage;
|
||||
@@ -950,7 +951,12 @@ function normalizeLocalCpaStep9Mode(value = '') {
|
||||
}
|
||||
|
||||
function normalizeHotmailServiceMode(value = '') {
|
||||
return HOTMAIL_SERVICE_MODE_LOCAL;
|
||||
if (typeof normalizeHotmailServiceModeFromUtils === 'function') {
|
||||
return normalizeHotmailServiceModeFromUtils(value);
|
||||
}
|
||||
return String(value || '').trim().toLowerCase() === HOTMAIL_SERVICE_MODE_REMOTE
|
||||
? HOTMAIL_SERVICE_MODE_REMOTE
|
||||
: HOTMAIL_SERVICE_MODE_LOCAL;
|
||||
}
|
||||
|
||||
function getSelectedLocalCpaStep9Mode() {
|
||||
@@ -975,10 +981,9 @@ function getSelectedHotmailServiceMode() {
|
||||
function setHotmailServiceMode(mode) {
|
||||
const resolvedMode = normalizeHotmailServiceMode(mode);
|
||||
hotmailServiceModeButtons.forEach((button) => {
|
||||
const isRemoteMode = button.dataset.hotmailServiceMode === HOTMAIL_SERVICE_MODE_REMOTE;
|
||||
const active = button.dataset.hotmailServiceMode === resolvedMode;
|
||||
button.disabled = isRemoteMode;
|
||||
button.setAttribute('aria-disabled', String(isRemoteMode));
|
||||
button.disabled = false;
|
||||
button.setAttribute('aria-disabled', 'false');
|
||||
button.classList.toggle('is-active', active);
|
||||
button.setAttribute('aria-pressed', String(active));
|
||||
});
|
||||
@@ -2457,7 +2462,7 @@ btnToggleHotmailList?.addEventListener('click', () => {
|
||||
btnHotmailUsageGuide?.addEventListener('click', async () => {
|
||||
await openConfirmModal({
|
||||
title: '使用教程',
|
||||
message: '由于第三方服务接口结构不同,所以暂时无法使用,如果您的token可以直连微软,请测试本地功能,详细功能请看项目根目录的readme文件',
|
||||
message: 'API对接模式会直接调用微软邮箱接口取件;本地助手模式仍走本地服务。两种模式继续共用同一套 Hotmail 账号池与导入格式。',
|
||||
confirmLabel: '确定',
|
||||
confirmVariant: 'btn-primary',
|
||||
});
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
test('Hotmail API对接界面文案应启用 remote 模式并隐藏禁用态', () => {
|
||||
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
||||
|
||||
assert.match(
|
||||
html,
|
||||
/data-hotmail-service-mode="remote"[^>]*>\s*API对接\s*<\/button>/,
|
||||
'remote 模式按钮应显示为 API对接'
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
html,
|
||||
/data-hotmail-service-mode="remote"[^>]*\sdisabled(\s|>)/,
|
||||
'API对接按钮不应继续被禁用'
|
||||
);
|
||||
assert.match(
|
||||
html,
|
||||
/<span class="data-label">API对接<\/span>/,
|
||||
'Hotmail 配置行文案应改为 API对接'
|
||||
);
|
||||
});
|
||||
|
||||
test('Hotmail API对接应接入微软邮箱 helper 而不是旧远程服务占位', () => {
|
||||
const background = fs.readFileSync('background.js', 'utf8');
|
||||
|
||||
assert.match(background, /importScripts\([^)]*microsoft-email\.js[^)]*\)/, 'background 应加载微软邮箱 helper');
|
||||
assert.match(background, /fetchMicrosoftVerificationCode/, '步骤 4\/7 应接入微软验证码读取 helper');
|
||||
assert.match(background, /fetchMicrosoftMailboxMessages/, '账号校验和最新验证码应接入微软邮箱列表 helper');
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
test('manifest 应声明 declarativeNetRequest 权限用于微软 token 接口跨域头处理', () => {
|
||||
const manifest = JSON.parse(fs.readFileSync('manifest.json', 'utf8'));
|
||||
|
||||
assert.equal(
|
||||
Array.isArray(manifest.permissions) && manifest.permissions.includes('declarativeNetRequest'),
|
||||
true,
|
||||
'manifest 缺少 declarativeNetRequest 权限'
|
||||
);
|
||||
});
|
||||
|
||||
test('background 应注册删除 Origin 请求头的动态规则', () => {
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
|
||||
assert.match(source, /setupDeclarativeNetRequestRules\(\)/, '应初始化跨域请求头规则');
|
||||
assert.match(source, /chrome\.declarativeNetRequest\.updateDynamicRules\(/, '应使用 declarativeNetRequest 动态规则');
|
||||
assert.match(source, /header:\s*'Origin'\s*,\s*operation:\s*'remove'/, '应删除 Origin 请求头');
|
||||
assert.match(source, /login\.microsoftonline\.com\/\*\/oauth2\/v2\.0\/token/, '规则应只命中微软 token 接口');
|
||||
assert.match(source, /resourceTypes:\s*\[\s*'xmlhttprequest'\s*\]/, '规则应限制在 xmlhttprequest');
|
||||
});
|
||||
@@ -12,6 +12,7 @@ const {
|
||||
getHotmailMailApiRequestConfig,
|
||||
getHotmailVerificationPollConfig,
|
||||
getHotmailVerificationRequestTimestamp,
|
||||
normalizeHotmailServiceMode,
|
||||
normalizeHotmailMailApiMessages,
|
||||
parseHotmailImportText,
|
||||
pickHotmailAccountForRun,
|
||||
@@ -461,6 +462,14 @@ test('getHotmailMailApiRequestConfig defines third-party mail API request defaul
|
||||
});
|
||||
});
|
||||
|
||||
test('normalizeHotmailServiceMode supports API对接 remote 模式并默认回退到本地助手', () => {
|
||||
assert.equal(normalizeHotmailServiceMode('remote'), 'remote');
|
||||
assert.equal(normalizeHotmailServiceMode('REMOTE'), 'remote');
|
||||
assert.equal(normalizeHotmailServiceMode('local'), 'local');
|
||||
assert.equal(normalizeHotmailServiceMode(''), 'local');
|
||||
assert.equal(normalizeHotmailServiceMode('unknown'), 'local');
|
||||
});
|
||||
|
||||
test('parseHotmailImportText parses account lines in email----password----clientId----token format', () => {
|
||||
const parsed = parseHotmailImportText(`
|
||||
账号----密码----ID----Token
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const {
|
||||
extractVerificationCodeFromMessages,
|
||||
fetchMicrosoftMailboxMessages,
|
||||
fetchMicrosoftVerificationCode,
|
||||
} = require('../microsoft-email.js');
|
||||
|
||||
test('extractVerificationCodeFromMessages 只命中过滤时间后的 OpenAI 微软邮件', () => {
|
||||
const result = extractVerificationCodeFromMessages([
|
||||
{
|
||||
From: { EmailAddress: { Address: 'noreply@openai.com' } },
|
||||
Subject: 'Your code is 112233',
|
||||
BodyPreview: '112233',
|
||||
ReceivedDateTime: '2026-04-14T09:00:00.000Z',
|
||||
Id: 'too-old',
|
||||
},
|
||||
{
|
||||
From: { EmailAddress: { Address: 'alerts@example.com' } },
|
||||
Subject: 'Your code is 223344',
|
||||
BodyPreview: '223344',
|
||||
ReceivedDateTime: '2026-04-14T10:00:00.000Z',
|
||||
Id: 'wrong-sender',
|
||||
},
|
||||
{
|
||||
From: { EmailAddress: { Address: 'account-security@openai.com' } },
|
||||
Subject: 'OpenAI verification',
|
||||
BodyPreview: 'Use 334455 to continue',
|
||||
ReceivedDateTime: '2026-04-14T10:05:00.000Z',
|
||||
Id: 'matched',
|
||||
},
|
||||
], {
|
||||
filterAfterTimestamp: Date.UTC(2026, 3, 14, 9, 30, 0),
|
||||
});
|
||||
|
||||
assert.deepEqual(result, {
|
||||
code: '334455',
|
||||
emailTimestamp: Date.UTC(2026, 3, 14, 10, 5, 0),
|
||||
messageId: 'matched',
|
||||
sender: 'account-security@openai.com',
|
||||
subject: 'OpenAI verification',
|
||||
});
|
||||
});
|
||||
|
||||
test('fetchMicrosoftMailboxMessages 使用 refresh token 拉取微软邮箱列表并返回新 token', async () => {
|
||||
const requests = [];
|
||||
const fetchImpl = async (url, options = {}) => {
|
||||
requests.push({ url, options });
|
||||
if (String(url).includes('/oauth2/v2.0/token')) {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
access_token: 'access-token-1',
|
||||
refresh_token: 'refresh-token-next',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
value: [
|
||||
{
|
||||
From: { EmailAddress: { Address: 'noreply@openai.com' } },
|
||||
Subject: 'OpenAI verification',
|
||||
BodyPreview: 'Use 445566 to continue',
|
||||
ReceivedDateTime: '2026-04-14T10:06:00.000Z',
|
||||
Id: 'mail-1',
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
const result = await fetchMicrosoftMailboxMessages({
|
||||
clientId: 'client-1',
|
||||
refreshToken: 'refresh-token-1',
|
||||
top: 5,
|
||||
fetchImpl,
|
||||
});
|
||||
|
||||
assert.equal(requests.length, 2);
|
||||
assert.equal(result.nextRefreshToken, 'refresh-token-next');
|
||||
assert.equal(result.messages.length, 1);
|
||||
assert.equal(result.messages[0].id, 'mail-1');
|
||||
});
|
||||
|
||||
test('fetchMicrosoftVerificationCode 会重试直到命中最新验证码', async () => {
|
||||
let messageRequestCount = 0;
|
||||
const logs = [];
|
||||
const fetchImpl = async (url) => {
|
||||
if (String(url).includes('/oauth2/v2.0/token')) {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
access_token: 'access-token-2',
|
||||
refresh_token: 'refresh-token-next-2',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
messageRequestCount += 1;
|
||||
if (messageRequestCount === 1) {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
value: [{
|
||||
From: { EmailAddress: { Address: 'alerts@example.com' } },
|
||||
Subject: 'Nothing useful',
|
||||
BodyPreview: 'No code',
|
||||
ReceivedDateTime: '2026-04-14T10:00:00.000Z',
|
||||
Id: 'mail-ignore',
|
||||
}],
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
value: [{
|
||||
From: { EmailAddress: { Address: 'noreply@openai.com' } },
|
||||
Subject: 'Your verification code',
|
||||
BodyPreview: '667788',
|
||||
ReceivedDateTime: '2026-04-14T10:10:00.000Z',
|
||||
Id: 'mail-hit',
|
||||
}],
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
const result = await fetchMicrosoftVerificationCode({
|
||||
token: 'refresh-token-2',
|
||||
clientId: 'client-2',
|
||||
maxRetries: 2,
|
||||
retryDelayMs: 0,
|
||||
fetchImpl,
|
||||
log: (message) => logs.push(message),
|
||||
filterAfterTimestamp: Date.UTC(2026, 3, 14, 9, 0, 0),
|
||||
});
|
||||
|
||||
assert.equal(result.code, '667788');
|
||||
assert.equal(result.messageId, 'mail-hit');
|
||||
assert.equal(result.nextRefreshToken, 'refresh-token-next-2');
|
||||
assert.equal(logs.some((message) => /retrying/i.test(message)), true);
|
||||
});
|
||||
Reference in New Issue
Block a user