Просмотр исходного кода

feat: support direct Microsoft API polling for Hotmail account pool

- 合并 PR #69 的核心改动:为 Hotmail 账号池启用 API 对接模式,并新增微软邮箱 helper 与 token 请求头处理
- 本地补充修复:合并最新 dev 后恢复 INBOX/Junk 逐邮箱夹拉取,以及现有验证码筛选与时间回退逻辑
- 影响范围:background Hotmail 轮询、sidepanel Hotmail 模式切换、microsoft-email helper、Hotmail 相关回归测试
QLHazyCoder 4 месяцев назад
Родитель
Сommit
d4f4731bca

+ 73 - 55
background.js

@@ -1,15 +1,23 @@
 // background.js — Service Worker: orchestration, state, tab management, message routing
 // background.js — Service Worker: orchestration, state, tab management, message routing
 
 
-importScripts('data/names.js', 'hotmail-utils.js', 'luckmail-utils.js', 'cloudflare-temp-email-utils.js', 'icloud-utils.js', 'content/activation-utils.js');
+importScripts(
+  'data/names.js',
+  'hotmail-utils.js',
+  'microsoft-email.js',
+  'luckmail-utils.js',
+  'cloudflare-temp-email-utils.js',
+  'icloud-utils.js',
+  'content/activation-utils.js'
+);
 
 
 const {
 const {
-  buildHotmailMailApiLatestUrl,
   extractVerificationCodeFromMessage,
   extractVerificationCodeFromMessage,
   filterHotmailAccountsByUsage,
   filterHotmailAccountsByUsage,
   getLatestHotmailMessage,
   getLatestHotmailMessage,
   getHotmailMailApiRequestConfig,
   getHotmailMailApiRequestConfig,
   getHotmailVerificationPollConfig,
   getHotmailVerificationPollConfig,
   getHotmailVerificationRequestTimestamp,
   getHotmailVerificationRequestTimestamp,
+  normalizeHotmailServiceMode,
   normalizeHotmailMailApiMessages,
   normalizeHotmailMailApiMessages,
   pickHotmailAccountForRun,
   pickHotmailAccountForRun,
   pickVerificationMessage,
   pickVerificationMessage,
@@ -17,6 +25,9 @@ const {
   pickVerificationMessageWithTimeFallback,
   pickVerificationMessageWithTimeFallback,
   shouldClearHotmailCurrentSelection,
   shouldClearHotmailCurrentSelection,
 } = self.HotmailUtils;
 } = self.HotmailUtils;
+const {
+  fetchMicrosoftMailboxMessages,
+} = self.MultiPageMicrosoftEmail;
 const {
 const {
   DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME,
   DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME,
   DEFAULT_LUCKMAIL_BASE_URL,
   DEFAULT_LUCKMAIL_BASE_URL,
@@ -114,9 +125,37 @@ const DEFAULT_HOTMAIL_LOCAL_BASE_URL = 'http://127.0.0.1:17373';
 const HOTMAIL_LOCAL_HELPER_TIMEOUT_MS = 45000;
 const HOTMAIL_LOCAL_HELPER_TIMEOUT_MS = 45000;
 const DEFAULT_LUCKMAIL_PROJECT_CODE = 'openai';
 const DEFAULT_LUCKMAIL_PROJECT_CODE = 'openai';
 const DISPLAY_TIMEZONE = 'Asia/Shanghai';
 const DISPLAY_TIMEZONE = 'Asia/Shanghai';
+const MICROSOFT_TOKEN_DNR_RULE_ID = 1001;
 const PERSISTENT_ALIAS_STATE_KEYS = ['manualAliasUsage', 'preservedAliases'];
 const PERSISTENT_ALIAS_STATE_KEYS = ['manualAliasUsage', 'preservedAliases'];
 
 
 initializeSessionStorageAccess();
 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)
 // 状态管理(chrome.storage.session + chrome.storage.local)
@@ -412,10 +451,6 @@ function normalizeCloudflareDomains(values) {
   return normalizedDomains;
   return normalizedDomains;
 }
 }
 
 
-function normalizeHotmailServiceMode(rawValue = '') {
-  return HOTMAIL_SERVICE_MODE_LOCAL;
-}
-
 function normalizeHotmailRemoteBaseUrl(rawValue = '') {
 function normalizeHotmailRemoteBaseUrl(rawValue = '') {
   const value = String(rawValue || '').trim();
   const value = String(rawValue || '').trim();
   if (!value) return DEFAULT_HOTMAIL_REMOTE_BASE_URL;
   if (!value) return DEFAULT_HOTMAIL_REMOTE_BASE_URL;
@@ -1180,11 +1215,6 @@ async function ensureHotmailAccountForFlow(options = {}) {
   return setCurrentHotmailAccount(account.id, { markUsed, syncEmail: true });
   return setCurrentHotmailAccount(account.id, { markUsed, syncEmail: true });
 }
 }
 
 
-function buildHotmailRemoteEndpoint(baseUrl, path) {
-  const normalizedBaseUrl = normalizeHotmailRemoteBaseUrl(baseUrl);
-  return new URL(path, `${normalizedBaseUrl}/`).toString();
-}
-
 function buildHotmailLocalEndpoint(baseUrl, path) {
 function buildHotmailLocalEndpoint(baseUrl, path) {
   const normalizedBaseUrl = normalizeHotmailLocalBaseUrl(baseUrl);
   const normalizedBaseUrl = normalizeHotmailLocalBaseUrl(baseUrl);
   return new URL(path, `${normalizedBaseUrl}/`).toString();
   return new URL(path, `${normalizedBaseUrl}/`).toString();
@@ -1201,55 +1231,40 @@ async function requestHotmailRemoteMailbox(account, mailbox = 'INBOX') {
     throw new Error(`Hotmail 账号 ${account.email || account.id} 缺少刷新令牌(refresh token)。`);
     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 { timeoutMs } = getHotmailMailApiRequestConfig();
   const controller = new AbortController();
   const controller = new AbortController();
   const timeoutId = setTimeout(() => controller.abort(new Error('timeout')), timeoutMs);
   const timeoutId = setTimeout(() => controller.abort(new Error('timeout')), timeoutMs);
 
 
-  let response;
   try {
   try {
-    response = await fetch(url, { method: 'GET', signal: controller.signal });
+    const result = await fetchMicrosoftMailboxMessages({
+      clientId: account.clientId,
+      refreshToken: account.refreshToken,
+      mailbox,
+      top: 10,
+      signal: controller.signal,
+    });
+
+    return {
+      mailbox,
+      payload: {
+        source: 'microsoft-api',
+        transport: result.transport,
+        tokenStrategy: result.tokenStrategy,
+      },
+      messages: normalizeHotmailMailApiMessages(result.messages).map((message) => ({
+        ...message,
+        mailbox: message?.mailbox || mailbox,
+      })),
+      nextRefreshToken: result.nextRefreshToken,
+    };
   } catch (err) {
   } catch (err) {
     if (err?.name === 'AbortError') {
     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 {
   } finally {
     clearTimeout(timeoutId);
     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) {
 function applyHotmailApiResultToAccount(account, apiResult) {
@@ -1282,7 +1297,10 @@ async function fetchHotmailMailboxMessagesFromRemoteService(account, mailboxes =
       mailboxResults.push({
       mailboxResults.push({
         mailbox,
         mailbox,
         count: result.messages.length,
         count: result.messages.length,
-        messages: result.messages.map((message) => ({ ...message, mailbox })),
+        messages: result.messages.map((message) => ({
+          ...message,
+          mailbox: message?.mailbox || mailbox,
+        })),
       });
       });
     }
     }
   } catch (err) {
   } catch (err) {
@@ -1592,7 +1610,7 @@ async function pollHotmailVerificationCode(step, state, pollPayload = {}) {
   for (let attempt = 1; attempt <= maxAttempts; attempt++) {
   for (let attempt = 1; attempt <= maxAttempts; attempt++) {
     throwIfStopped();
     throwIfStopped();
     try {
     try {
-      await addLog(`步骤 ${step}:正在轮询 Hotmail 邮件(${attempt}/${maxAttempts})...`, 'info');
+      await addLog(`步骤 ${step}:正在通过 API对接 轮询 Hotmail 邮件(${attempt}/${maxAttempts})...`, 'info');
       const fetchResult = await fetchHotmailMailboxMessages(account, HOTMAIL_MAILBOXES);
       const fetchResult = await fetchHotmailMailboxMessages(account, HOTMAIL_MAILBOXES);
       account = fetchResult.account;
       account = fetchResult.account;
       const matchResult = pickVerificationMessageWithTimeFallback(fetchResult.messages, {
       const matchResult = pickVerificationMessageWithTimeFallback(fetchResult.messages, {
@@ -1609,7 +1627,7 @@ async function pollHotmailVerificationCode(step, state, pollPayload = {}) {
           const fallbackLabel = matchResult.usedTimeFallback ? '宽松匹配 + 时间回退' : '宽松匹配';
           const fallbackLabel = matchResult.usedTimeFallback ? '宽松匹配 + 时间回退' : '宽松匹配';
           await addLog(`步骤 ${step}:严格规则未命中,已改用 ${fallbackLabel} 并命中 Hotmail ${mailboxLabel} 验证码。`, 'warn');
           await addLog(`步骤 ${step}:严格规则未命中,已改用 ${fallbackLabel} 并命中 Hotmail ${mailboxLabel} 验证码。`, 'warn');
         }
         }
-        await addLog(`步骤 ${step}:已在 Hotmail ${mailboxLabel} 中找到验证码:${match.code}`, 'ok');
+        await addLog(`步骤 ${step}:已通过 API对接 在 Hotmail ${mailboxLabel} 中找到验证码:${match.code}`, 'ok');
         return {
         return {
           ok: true,
           ok: true,
           code: match.code,
           code: match.code,
@@ -1626,7 +1644,7 @@ async function pollHotmailVerificationCode(step, state, pollPayload = {}) {
       }
       }
     } catch (err) {
     } catch (err) {
       lastError = err;
       lastError = err;
-      await addLog(`步骤 ${step}:Hotmail 收件箱轮询失败:${err.message}`, 'warn');
+      await addLog(`步骤 ${step}:Hotmail API 对接轮询失败:${err.message}`, 'warn');
     }
     }
 
 
     if (attempt < maxAttempts) {
     if (attempt < maxAttempts) {
@@ -3720,7 +3738,7 @@ function getSourceLabel(source) {
     'mail-2925': '2925 邮箱',
     'mail-2925': '2925 邮箱',
     'inbucket-mail': 'Inbucket 邮箱',
     'inbucket-mail': 'Inbucket 邮箱',
     'duck-mail': 'Duck 邮箱',
     'duck-mail': 'Duck 邮箱',
-    'hotmail-api': 'Hotmail(远程/本地)',
+    'hotmail-api': 'Hotmail(API对接/本地助手)',
     'luckmail-api': 'LuckMail(API 购邮)',
     'luckmail-api': 'LuckMail(API 购邮)',
     'cloudflare-temp-email': 'Cloudflare Temp Email',
     'cloudflare-temp-email': 'Cloudflare Temp Email',
   };
   };
@@ -6514,7 +6532,7 @@ function getMailConfig(state) {
     return { provider: 'custom', label: '自定义邮箱' };
     return { provider: 'custom', label: '自定义邮箱' };
   }
   }
   if (provider === HOTMAIL_PROVIDER) {
   if (provider === HOTMAIL_PROVIDER) {
-    return { provider: HOTMAIL_PROVIDER, label: 'Hotmail(远程/本地)' };
+    return { provider: HOTMAIL_PROVIDER, label: 'Hotmail(API对接/本地助手)' };
   }
   }
   if (provider === LUCKMAIL_PROVIDER) {
   if (provider === LUCKMAIL_PROVIDER) {
     return { provider: LUCKMAIL_PROVIDER, label: 'LuckMail(API 购邮)' };
     return { provider: LUCKMAIL_PROVIDER, label: 'LuckMail(API 购邮)' };

+ 9 - 0
hotmail-utils.js

@@ -7,6 +7,8 @@
   root.HotmailUtils = factory();
   root.HotmailUtils = factory();
 })(typeof self !== 'undefined' ? self : globalThis, function createHotmailUtils() {
 })(typeof self !== 'undefined' ? self : globalThis, function createHotmailUtils() {
   const HOTMAIL_MAIL_API_URL = 'https://apple.882263.xyz/api/mail-new';
   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) {
   function normalizeText(value) {
     return String(value || '')
     return String(value || '')
@@ -25,6 +27,12 @@
     return Number.isFinite(timestamp) ? timestamp : 0;
     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) {
   function extractVerificationCode(text) {
     const source = String(text || '');
     const source = String(text || '');
     const matchCn = source.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/i);
     const matchCn = source.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/i);
@@ -368,6 +376,7 @@
     getHotmailVerificationPollConfig,
     getHotmailVerificationPollConfig,
     getHotmailVerificationRequestTimestamp,
     getHotmailVerificationRequestTimestamp,
     isAuthorizedHotmailAccount,
     isAuthorizedHotmailAccount,
+    normalizeHotmailServiceMode,
     normalizeHotmailMailApiMessages,
     normalizeHotmailMailApiMessages,
     normalizeTimestamp,
     normalizeTimestamp,
     parseHotmailImportText,
     parseHotmailImportText,

+ 451 - 0
microsoft-email.js

@@ -0,0 +1,451 @@
+(function attachMicrosoftEmailHelpers(globalScope) {
+  const OPENAI_SENDER_PATTERNS = [
+    /openai\.com/i,
+    /auth0\.openai\.com/i,
+  ];
+  const CODE_PATTERN = /\b(\d{6})\b/;
+  const GRAPH_SCOPES = 'offline_access https://graph.microsoft.com/Mail.Read https://graph.microsoft.com/User.Read';
+  const GRAPH_DEFAULT_SCOPE = 'https://graph.microsoft.com/.default';
+  const TOKEN_STRATEGIES = [
+    {
+      name: 'entra-common-delegated',
+      url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
+      extraData: { scope: GRAPH_SCOPES },
+    },
+    {
+      name: 'entra-consumers-delegated',
+      url: 'https://login.microsoftonline.com/consumers/oauth2/v2.0/token',
+      extraData: { scope: GRAPH_SCOPES },
+    },
+    {
+      name: 'entra-common-default',
+      url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
+      extraData: { scope: GRAPH_DEFAULT_SCOPE },
+    },
+    {
+      name: 'entra-common-outlook',
+      url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
+      extraData: {},
+    },
+  ];
+  const TRANSPORT_PLANS = [
+    {
+      transport: 'graph',
+      strategyNames: ['entra-common-delegated', 'entra-consumers-delegated', 'entra-common-default'],
+    },
+    {
+      transport: 'outlook',
+      strategyNames: ['entra-common-outlook', 'entra-common-delegated', 'entra-consumers-delegated'],
+    },
+  ];
+  const GRAPH_API_BASE = 'https://graph.microsoft.com/v1.0/me/mailFolders';
+  const OUTLOOK_API_BASE = 'https://outlook.office.com/api/v2.0/me/mailfolders';
+
+  function getFetchImpl(fetchImpl) {
+    const resolved = fetchImpl || globalScope.fetch;
+    if (typeof resolved !== 'function') {
+      throw new Error('Microsoft email helper requires a fetch implementation.');
+    }
+    return resolved;
+  }
+
+  function resolveTokenStrategy(name) {
+    return TOKEN_STRATEGIES.find((item) => item.name === name) || TOKEN_STRATEGIES[0];
+  }
+
+  function normalizeMailboxLabel(mailbox = 'INBOX') {
+    return /^junk(?:\s*e-?mail|\s*email)?$/i.test(String(mailbox || '').trim()) ? 'Junk' : 'INBOX';
+  }
+
+  function normalizeMailboxId(mailbox = 'INBOX') {
+    return normalizeMailboxLabel(mailbox) === 'Junk' ? 'junkemail' : 'inbox';
+  }
+
+  function normalizeMailboxList(mailboxes) {
+    const list = Array.isArray(mailboxes) && mailboxes.length ? mailboxes : ['INBOX'];
+    return [...new Set(list.map((mailbox) => normalizeMailboxLabel(mailbox)))];
+  }
+
+  async function getResponseErrorText(response) {
+    const text = await response.text().catch(() => '');
+    if (!text) {
+      return response.statusText || `HTTP ${response.status}`;
+    }
+    try {
+      const parsed = JSON.parse(text);
+      return parsed.error_description || parsed.error?.message || parsed.error || parsed.message || text;
+    } catch {
+      return text;
+    }
+  }
+
+  async function exchangeRefreshToken(clientId, refreshToken, options = {}) {
+    const fetchImpl = getFetchImpl(options.fetchImpl);
+    const strategy = resolveTokenStrategy(options.strategyName);
+    const body = new URLSearchParams({
+      client_id: clientId,
+      grant_type: 'refresh_token',
+      refresh_token: refreshToken,
+      ...(strategy.extraData || {}),
+    });
+    const response = await fetchImpl(strategy.url, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+      body: body.toString(),
+      signal: options.signal,
+    });
+
+    if (!response.ok) {
+      throw new Error(`${strategy.name}: ${await getResponseErrorText(response)}`);
+    }
+
+    const data = await response.json();
+    if (!data.access_token) {
+      throw new Error(`${strategy.name}: token response missing access_token`);
+    }
+
+    return {
+      ...data,
+      tokenStrategy: strategy.name,
+    };
+  }
+
+  async function fetchGraphMessages(accessToken, options = {}) {
+    const fetchImpl = getFetchImpl(options.fetchImpl);
+    const mailbox = normalizeMailboxLabel(options.mailbox);
+    const top = Math.max(1, Math.min(Number(options.top) || 5, 30));
+    const url = `${GRAPH_API_BASE}/${normalizeMailboxId(mailbox)}/messages?$top=${encodeURIComponent(top)}&$select=id,internetMessageId,subject,from,bodyPreview,receivedDateTime&$orderby=receivedDateTime desc`;
+    const response = await fetchImpl(url, {
+      method: 'GET',
+      headers: {
+        Accept: 'application/json',
+        Authorization: `Bearer ${accessToken}`,
+      },
+      signal: options.signal,
+    });
+
+    if (!response.ok) {
+      throw new Error(`graph: ${await getResponseErrorText(response)}`);
+    }
+
+    const payload = await response.json();
+    return Array.isArray(payload?.value) ? payload.value : [];
+  }
+
+  async function fetchOutlookMessages(accessToken, options = {}) {
+    const fetchImpl = getFetchImpl(options.fetchImpl);
+    const mailbox = normalizeMailboxLabel(options.mailbox);
+    const top = Math.max(1, Math.min(Number(options.top) || 5, 30));
+    const url = `${OUTLOOK_API_BASE}/${normalizeMailboxId(mailbox)}/messages?$top=${encodeURIComponent(top)}&$select=Id,Subject,From,BodyPreview,Body,ReceivedDateTime&$orderby=ReceivedDateTime desc`;
+    const response = await fetchImpl(url, {
+      method: 'GET',
+      headers: {
+        Accept: 'application/json',
+        Authorization: `Bearer ${accessToken}`,
+      },
+      signal: options.signal,
+    });
+
+    if (!response.ok) {
+      throw new Error(`outlook: ${await getResponseErrorText(response)}`);
+    }
+
+    const payload = await response.json();
+    return Array.isArray(payload?.value) ? payload.value : [];
+  }
+
+  function normalizeMessage(message, mailbox = 'INBOX') {
+    const sender = message?.From || message?.from || {};
+    const emailAddress = sender?.EmailAddress || sender?.emailAddress || {};
+    return {
+      mailbox: normalizeMailboxLabel(mailbox || message?.mailbox),
+      from: {
+        emailAddress: {
+          address: String(emailAddress?.Address || emailAddress?.address || '').trim(),
+          name: String(emailAddress?.Name || emailAddress?.name || '').trim(),
+        },
+      },
+      subject: String(message?.Subject || message?.subject || '').trim(),
+      receivedDateTime: String(message?.ReceivedDateTime || message?.receivedDateTime || '').trim(),
+      bodyPreview: String(message?.BodyPreview || message?.bodyPreview || '').trim(),
+      body: {
+        content: String(message?.Body?.Content || message?.body?.content || '').trim(),
+      },
+      id: String(message?.Id || message?.id || message?.internetMessageId || '').trim(),
+    };
+  }
+
+  function normalizeFilterValue(value) {
+    return String(value || '').trim().toLowerCase();
+  }
+
+  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 = Number(options.filterAfterTimestamp || 0) || 0;
+    const senderFilters = (options.senderFilters || []).map(normalizeFilterValue).filter(Boolean);
+    const subjectFilters = (options.subjectFilters || []).map(normalizeFilterValue).filter(Boolean);
+    const excludedCodes = new Set((options.excludeCodes || []).map((value) => String(value || '').trim()).filter(Boolean));
+    const hasExplicitFilters = senderFilters.length > 0 || subjectFilters.length > 0;
+
+    const sortedMessages = (Array.isArray(messages) ? messages : [])
+      .map((raw) => normalizeMessage(raw, raw?.mailbox))
+      .sort((left, right) => getMessageTimestamp(right) - getMessageTimestamp(left));
+
+    for (const message of sortedMessages) {
+      const receivedAt = getMessageTimestamp(message);
+      if (receivedAt && receivedAt < filterAfterTimestamp) {
+        continue;
+      }
+
+      const sender = normalizeFilterValue(getMessageSender(message));
+      const subject = normalizeFilterValue(message?.subject);
+      const preview = normalizeFilterValue(message?.bodyPreview);
+      const searchText = normalizeFilterValue(getMessageSearchText(message));
+      const codeMatch = getMessageSearchText(message).match(CODE_PATTERN);
+      const code = codeMatch?.[1] || '';
+      if (!code || excludedCodes.has(code)) {
+        continue;
+      }
+
+      if (!hasExplicitFilters && !isOpenAiMessage(message)) {
+        continue;
+      }
+
+      const senderMatched = senderFilters.length === 0
+        ? true
+        : senderFilters.some((filter) => sender.includes(filter) || preview.includes(filter) || searchText.includes(filter));
+      const subjectMatched = subjectFilters.length === 0
+        ? true
+        : subjectFilters.some((filter) => subject.includes(filter) || preview.includes(filter) || searchText.includes(filter));
+      if (!senderMatched && !subjectMatched) {
+        continue;
+      }
+
+      return {
+        code,
+        emailTimestamp: receivedAt || Date.now(),
+        messageId: message?.id || null,
+        sender: getMessageSender(message),
+        subject: String(message?.subject || ''),
+        mailbox: message?.mailbox || 'INBOX',
+        message,
+      };
+    }
+
+    return null;
+  }
+
+  async function fetchMicrosoftMailboxMessages(options = {}) {
+    const {
+      clientId,
+      refreshToken,
+      mailbox = 'INBOX',
+      top = 5,
+      fetchImpl,
+      signal,
+      log = null,
+    } = options;
+
+    if (!refreshToken) {
+      throw new Error('Microsoft refresh token is empty.');
+    }
+    if (!clientId) {
+      throw new Error('Microsoft client_id is empty.');
+    }
+
+    const errors = [];
+    for (const plan of TRANSPORT_PLANS) {
+      for (const strategyName of plan.strategyNames) {
+        try {
+          const tokenData = await exchangeRefreshToken(clientId, refreshToken, {
+            fetchImpl,
+            signal,
+            strategyName,
+          });
+          const rawMessages = plan.transport === 'graph'
+            ? await fetchGraphMessages(tokenData.access_token, { mailbox, top, fetchImpl, signal })
+            : await fetchOutlookMessages(tokenData.access_token, { mailbox, top, fetchImpl, signal });
+
+          return {
+            tokenData,
+            nextRefreshToken: String(tokenData?.refresh_token || '').trim(),
+            tokenStrategy: strategyName,
+            transport: plan.transport,
+            mailbox: normalizeMailboxLabel(mailbox),
+            messages: rawMessages.map((message) => normalizeMessage(message, mailbox)),
+          };
+        } catch (error) {
+          const message = error?.message || String(error);
+          errors.push(`${plan.transport}/${strategyName}: ${message}`);
+          if (typeof log === 'function') {
+            log(`mailbox=${normalizeMailboxLabel(mailbox)} ${plan.transport}/${strategyName} failed: ${message}`);
+          }
+        }
+      }
+    }
+
+    throw new Error(`Microsoft mailbox request failed: ${errors.join(' | ')}`);
+  }
+
+  function delay(timeoutMs, signal) {
+    if (timeoutMs <= 0) {
+      return Promise.resolve();
+    }
+
+    return new Promise((resolve, reject) => {
+      const timer = setTimeout(() => {
+        cleanup();
+        resolve();
+      }, timeoutMs);
+      const onAbort = () => {
+        cleanup();
+        reject(signal.reason || new Error('Aborted'));
+      };
+      const cleanup = () => {
+        clearTimeout(timer);
+        signal?.removeEventListener('abort', onAbort);
+      };
+
+      if (signal?.aborted) {
+        cleanup();
+        reject(signal.reason || new Error('Aborted'));
+        return;
+      }
+
+      signal?.addEventListener('abort', onAbort, { once: true });
+    });
+  }
+
+  async function fetchMicrosoftVerificationCode(options = {}) {
+    const {
+      token,
+      refreshToken,
+      clientId,
+      maxRetries = 3,
+      retryDelayMs = 10000,
+      top = 5,
+      log = null,
+      filterAfterTimestamp = 0,
+      senderFilters = [],
+      subjectFilters = [],
+      excludeCodes = [],
+      mailboxes = ['INBOX'],
+      fetchImpl,
+      signal,
+    } = options;
+
+    let workingRefreshToken = String(refreshToken || token || '').trim();
+    if (!workingRefreshToken) {
+      throw new Error('Microsoft refresh token is empty.');
+    }
+    if (!clientId) {
+      throw new Error('Microsoft client_id is empty.');
+    }
+
+    const normalizedMailboxes = normalizeMailboxList(mailboxes);
+    let lastError = null;
+
+    for (let attempt = 1; attempt <= maxRetries; attempt += 1) {
+      try {
+        const collectedMessages = [];
+        for (const mailbox of normalizedMailboxes) {
+          const result = await fetchMicrosoftMailboxMessages({
+            clientId,
+            refreshToken: workingRefreshToken,
+            mailbox,
+            top,
+            fetchImpl,
+            signal,
+            log,
+          });
+          if (result.nextRefreshToken) {
+            workingRefreshToken = result.nextRefreshToken;
+          }
+          collectedMessages.push(...result.messages);
+        }
+
+        const match = extractVerificationCodeFromMessages(collectedMessages, {
+          filterAfterTimestamp,
+          senderFilters,
+          subjectFilters,
+          excludeCodes,
+        });
+        if (match) {
+          return {
+            ...match,
+            nextRefreshToken: workingRefreshToken,
+            messages: collectedMessages,
+          };
+        }
+
+        lastError = new Error('No matching Microsoft verification email found.');
+      } catch (error) {
+        lastError = error;
+      }
+
+      if (attempt < maxRetries) {
+        if (typeof log === 'function') {
+          log(`attempt ${attempt}/${maxRetries} found no matching Microsoft mail, retrying...`);
+        }
+        await delay(retryDelayMs, signal);
+      }
+    }
+
+    throw lastError || new Error('No matching Microsoft verification email found.');
+  }
+
+  const api = {
+    CODE_PATTERN,
+    exchangeRefreshToken,
+    extractVerificationCodeFromMessages,
+    fetchGraphMessages,
+    fetchMicrosoftMailboxMessages,
+    fetchMicrosoftVerificationCode,
+    fetchOutlookMessages,
+    getMessageSender,
+    getMessageTimestamp,
+    isOpenAiMessage,
+    normalizeMailboxId,
+    normalizeMailboxLabel,
+    normalizeMessage,
+  };
+
+  globalScope.MultiPageMicrosoftEmail = api;
+
+  if (typeof module !== 'undefined' && module.exports) {
+    module.exports = api;
+  }
+})(typeof globalThis !== 'undefined' ? globalThis : this);

+ 4 - 4
sidepanel/sidepanel.html

@@ -153,7 +153,7 @@
         <div class="data-inline">
         <div class="data-inline">
           <select id="select-mail-provider" class="data-select">
           <select id="select-mail-provider" class="data-select">
             <option value="custom">自定义邮箱</option>
             <option value="custom">自定义邮箱</option>
-            <option value="hotmail-api">Hotmail(本地助手)</option>
+            <option value="hotmail-api">Hotmail(账号池)</option>
             <option value="luckmail-api">LuckMail(API 购邮)</option>
             <option value="luckmail-api">LuckMail(API 购邮)</option>
             <option value="163">163 邮箱 (mail.163.com)</option>
             <option value="163">163 邮箱 (mail.163.com)</option>
             <option value="163-vip">163 VIP 邮箱 (vip.163.com)</option>
             <option value="163-vip">163 VIP 邮箱 (vip.163.com)</option>
@@ -303,13 +303,13 @@
       <div class="data-row" id="row-hotmail-service-mode">
       <div class="data-row" id="row-hotmail-service-mode">
         <span class="data-label">接码模式</span>
         <span class="data-label">接码模式</span>
         <div id="hotmail-service-mode-group" class="choice-group" role="group" aria-label="Hotmail 接码模式">
         <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>
           <button type="button" class="choice-btn" data-hotmail-service-mode="local">本地助手</button>
         </div>
         </div>
       </div>
       </div>
       <div class="data-row" id="row-hotmail-remote-base-url">
       <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>
       <div class="data-row" id="row-hotmail-local-base-url" style="display:none;">
       <div class="data-row" id="row-hotmail-local-base-url" style="display:none;">
         <span class="data-label">本地助手</span>
         <span class="data-label">本地助手</span>

+ 10 - 5
sidepanel/sidepanel.js

@@ -240,6 +240,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 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 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 parseHotmailImportText = window.HotmailUtils?.parseHotmailImportText;
+const normalizeHotmailServiceModeFromUtils = window.HotmailUtils?.normalizeHotmailServiceMode;
 const shouldClearHotmailCurrentSelection = window.HotmailUtils?.shouldClearHotmailCurrentSelection;
 const shouldClearHotmailCurrentSelection = window.HotmailUtils?.shouldClearHotmailCurrentSelection;
 const upsertHotmailAccountInList = window.HotmailUtils?.upsertHotmailAccountInList;
 const upsertHotmailAccountInList = window.HotmailUtils?.upsertHotmailAccountInList;
 const filterHotmailAccountsByUsage = window.HotmailUtils?.filterHotmailAccountsByUsage;
 const filterHotmailAccountsByUsage = window.HotmailUtils?.filterHotmailAccountsByUsage;
@@ -1138,7 +1139,12 @@ function normalizeMail2925Mode(value = '') {
 }
 }
 
 
 function normalizeHotmailServiceMode(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() {
 function getSelectedLocalCpaStep9Mode() {
@@ -1177,10 +1183,9 @@ function getSelectedHotmailServiceMode() {
 function setHotmailServiceMode(mode) {
 function setHotmailServiceMode(mode) {
   const resolvedMode = normalizeHotmailServiceMode(mode);
   const resolvedMode = normalizeHotmailServiceMode(mode);
   hotmailServiceModeButtons.forEach((button) => {
   hotmailServiceModeButtons.forEach((button) => {
-    const isRemoteMode = button.dataset.hotmailServiceMode === HOTMAIL_SERVICE_MODE_REMOTE;
     const active = button.dataset.hotmailServiceMode === resolvedMode;
     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.classList.toggle('is-active', active);
     button.setAttribute('aria-pressed', String(active));
     button.setAttribute('aria-pressed', String(active));
   });
   });
@@ -3697,7 +3702,7 @@ btnToggleHotmailList?.addEventListener('click', () => {
 btnHotmailUsageGuide?.addEventListener('click', async () => {
 btnHotmailUsageGuide?.addEventListener('click', async () => {
   await openConfirmModal({
   await openConfirmModal({
     title: '使用教程',
     title: '使用教程',
-    message: '由于第三方服务接口结构不同,所以暂时无法使用,如果您的token可以直连微软,请测试本地功能,详细功能请看项目根目录的readme文件',
+    message: 'API对接模式会直接调用微软邮箱接口取件;本地助手模式仍走本地服务。两种模式继续共用同一套 Hotmail 账号池与导入格式。',
     confirmLabel: '确定',
     confirmLabel: '确定',
     confirmVariant: 'btn-primary',
     confirmVariant: 'btn-primary',
   });
   });

+ 45 - 0
tests/hotmail-api-mode.test.js

@@ -0,0 +1,45 @@
+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, /fetchMicrosoftMailboxMessages/, '账号校验和最新验证码应接入微软邮箱列表 helper');
+  assert.match(
+    background,
+    /fetchMicrosoftMailboxMessages\(\{[\s\S]*mailbox,[\s\S]*top:\s*10,/,
+    '微软邮箱 helper 应按请求的邮箱夹读取消息'
+  );
+  assert.match(
+    background,
+    /for \(const mailbox of mailboxes\) \{\s*const result = await requestHotmailRemoteMailbox\(workingAccount, mailbox\);/s,
+    'API对接模式不应丢掉 INBOX\/Junk 的逐邮箱夹轮询'
+  );
+  assert.match(
+    background,
+    /pickVerificationMessageWithTimeFallback\(fetchResult\.messages, \{/,
+    '步骤 4\/7 应继续复用现有验证码筛选与时间回退逻辑'
+  );
+});

+ 23 - 0
tests/hotmail-cors-headers.test.js

@@ -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');
+});

+ 9 - 0
tests/hotmail-utils.test.js

@@ -12,6 +12,7 @@ const {
   getHotmailMailApiRequestConfig,
   getHotmailMailApiRequestConfig,
   getHotmailVerificationPollConfig,
   getHotmailVerificationPollConfig,
   getHotmailVerificationRequestTimestamp,
   getHotmailVerificationRequestTimestamp,
+  normalizeHotmailServiceMode,
   normalizeHotmailMailApiMessages,
   normalizeHotmailMailApiMessages,
   parseHotmailImportText,
   parseHotmailImportText,
   pickHotmailAccountForRun,
   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', () => {
 test('parseHotmailImportText parses account lines in email----password----clientId----token format', () => {
   const parsed = parseHotmailImportText(`
   const parsed = parseHotmailImportText(`
 账号----密码----ID----Token
 账号----密码----ID----Token

+ 215 - 0
tests/microsoft-email.test.js

@@ -0,0 +1,215 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+
+const {
+  extractVerificationCodeFromMessages,
+  fetchMicrosoftMailboxMessages,
+  fetchMicrosoftVerificationCode,
+  normalizeMailboxId,
+} = require('../microsoft-email.js');
+
+test('extractVerificationCodeFromMessages 支持显式过滤条件并跳过排除的验证码', () => {
+  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),
+    senderFilters: ['openai'],
+    subjectFilters: ['verification'],
+    excludeCodes: ['112233'],
+  });
+
+  assert.deepEqual(result, {
+    code: '334455',
+    emailTimestamp: Date.UTC(2026, 3, 14, 10, 5, 0),
+    messageId: 'matched',
+    sender: 'account-security@openai.com',
+    subject: 'OpenAI verification',
+    mailbox: 'INBOX',
+    message: {
+      mailbox: 'INBOX',
+      from: {
+        emailAddress: {
+          address: 'account-security@openai.com',
+          name: '',
+        },
+      },
+      subject: 'OpenAI verification',
+      receivedDateTime: '2026-04-14T10:05:00.000Z',
+      bodyPreview: 'Use 334455 to continue',
+      body: {
+        content: '',
+      },
+      id: 'matched',
+    },
+  });
+});
+
+test('normalizeMailboxId 将 Junk 归一为微软邮箱夹 ID', () => {
+  assert.equal(normalizeMailboxId('INBOX'), 'inbox');
+  assert.equal(normalizeMailboxId('junk'), 'junkemail');
+  assert.equal(normalizeMailboxId('Junk Email'), 'junkemail');
+});
+
+test('fetchMicrosoftMailboxMessages 会回退到可用的 token 策略并保留邮箱夹信息', async () => {
+  const requests = [];
+  const fetchImpl = async (url, options = {}) => {
+    requests.push({ url, options });
+    if (String(url).includes('/oauth2/v2.0/token')) {
+      const params = new URLSearchParams(String(options.body || ''));
+      if (String(url).includes('/common/') && params.get('scope')?.includes('Mail.Read')) {
+        return {
+          ok: false,
+          status: 400,
+          statusText: 'Bad Request',
+          text: async () => JSON.stringify({ error_description: 'common delegated failed' }),
+        };
+      }
+      return {
+        ok: true,
+        json: async () => ({
+          access_token: 'access-token-1',
+          refresh_token: 'refresh-token-next',
+        }),
+      };
+    }
+
+    assert.match(String(url), /graph\.microsoft\.com\/v1\.0\/me\/mailFolders\/junkemail\/messages/);
+    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',
+    mailbox: 'Junk',
+    top: 5,
+    fetchImpl,
+  });
+
+  assert.equal(requests.length, 3);
+  assert.equal(result.nextRefreshToken, 'refresh-token-next');
+  assert.equal(result.tokenStrategy, 'entra-consumers-delegated');
+  assert.equal(result.transport, 'graph');
+  assert.equal(result.messages.length, 1);
+  assert.equal(result.messages[0].id, 'mail-1');
+  assert.equal(result.messages[0].mailbox, 'Junk');
+});
+
+test('fetchMicrosoftVerificationCode 会按邮箱夹轮询并在 Junk 中命中最新验证码', async () => {
+  const mailboxRequests = {
+    inbox: 0,
+    junkemail: 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',
+        }),
+      };
+    }
+
+    const urlString = String(url);
+    if (urlString.includes('/mailFolders/inbox/messages')) {
+      mailboxRequests.inbox += 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',
+          }],
+        }),
+      };
+    }
+
+    assert.match(urlString, /mailFolders\/junkemail\/messages/);
+    mailboxRequests.junkemail += 1;
+    if (mailboxRequests.junkemail === 1) {
+      return {
+        ok: true,
+        json: async () => ({
+          value: [{
+            from: { emailAddress: { address: 'no-reply@example.com' } },
+            subject: 'Nothing useful',
+            bodyPreview: 'Still no code',
+            receivedDateTime: '2026-04-14T10:05:00.000Z',
+            id: 'mail-ignore-2',
+          }],
+        }),
+      };
+    }
+
+    return {
+      ok: true,
+      json: async () => ({
+        value: [{
+          from: { emailAddress: { address: 'account-security@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,
+    mailboxes: ['INBOX', 'Junk'],
+    fetchImpl,
+    log: (message) => logs.push(message),
+    filterAfterTimestamp: Date.UTC(2026, 3, 14, 9, 0, 0),
+    senderFilters: ['openai'],
+    subjectFilters: ['verification'],
+  });
+
+  assert.equal(result.code, '667788');
+  assert.equal(result.messageId, 'mail-hit');
+  assert.equal(result.nextRefreshToken, 'refresh-token-next-2');
+  assert.equal(result.mailbox, 'Junk');
+  assert.equal(mailboxRequests.inbox, 2);
+  assert.equal(mailboxRequests.junkemail, 2);
+  assert.equal(logs.some((message) => /retrying/i.test(message)), true);
+});