Parcourir la source

第三方支持

QLHazyCoder il y a 4 mois
Parent
commit
36aeecf214
6 fichiers modifiés avec 191 ajouts et 281 suppressions
  1. 12 4
      README.md
  2. 83 202
      background.js
  3. 16 37
      hotmail-utils.js
  4. 18 2
      sidepanel/sidepanel.html
  5. 31 5
      sidepanel/sidepanel.js
  6. 31 31
      tests/hotmail-utils.test.js

+ 12 - 4
README.md

@@ -52,7 +52,7 @@
 - 支持自定义密码;留空时自动生成强密码
 - 自动显示当前使用中的密码,便于后续保存
 - 自动获取注册验证码与登录验证码
-- 支持 `Hotmail`:直接使用 `邮箱 + 客户端 ID + 刷新令牌(refresh token)` 刷新微软令牌,并通过 Microsoft Graph 读取最新邮件
+- 支持 `Hotmail`:直接使用 `邮箱 + 客户端 ID + 刷新令牌(refresh token)` 请求第三方邮件 API 读取最新邮件
 - 支持 `QQ Mail`、`163 Mail`、`Inbucket mailbox`
 - 支持从 DuckDuckGo Email Protection 自动生成新的 `@duck.com` 地址
 - 支持基于 Cloudflare 自定义域名自动生成随机邮箱前缀
@@ -137,7 +137,7 @@ Step 1 和 Step 9 都依赖这个地址。
 
 说明:
 
-- `Hotmail` 通过侧边栏里的 Hotmail 账号池选择账号,并直接访问 Microsoft Graph 邮件接口
+- `Hotmail` 通过侧边栏里的 Hotmail 账号池选择账号,并请求第三方邮件 API
 - `QQ`、`163`、`163 VIP` 用于直接轮询网页邮箱
 - `Inbucket` 通过你在侧边栏里配置的 host 访问 `mailbox` 页面:`https://<your-inbucket-host>/m/<mailbox>/`
 
@@ -145,6 +145,13 @@ Step 1 和 Step 9 都依赖这个地址。
 
 仅当 `Mail = Hotmail` 时使用。
 
+可配置项:
+
+- `Hotmail API 地址`
+- `响应类型`
+- `收件箱参数`
+- `垃圾箱参数`
+
 每条账号支持保存:
 
 - `email`
@@ -154,7 +161,8 @@ Step 1 和 Step 9 都依赖这个地址。
 
 使用方式:
 
-- 先新增账号
+- 先配置 Hotmail API 相关设置
+- 再新增账号
 - 点击 `校验`
 - 校验通过后,可点击 `测试收信`
 - Auto 模式每轮会自动选用一个可用账号
@@ -406,7 +414,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
 
 支持:
 
-- `Hotmail`(Microsoft Graph 邮件接口
+- `Hotmail`(第三方邮件 API
 - `content/qq-mail.js`
 - `content/mail-163.js`
 - `content/inbucket-mail.js`

+ 83 - 202
background.js

@@ -3,11 +3,11 @@
 importScripts('data/names.js', 'hotmail-utils.js', 'content/activation-utils.js');
 
 const {
-  buildHotmailGraphMessagesUrl,
+  buildHotmailMailApiLatestUrl,
   extractVerificationCodeFromMessage,
   filterHotmailAccountsByUsage,
   getLatestHotmailMessage,
-  getHotmailGraphRequestConfig,
+  getHotmailMailApiRequestConfig,
   getHotmailVerificationPollConfig,
   getHotmailVerificationRequestTimestamp,
   normalizeHotmailMailApiMessages,
@@ -20,8 +20,6 @@ const {
 const {
   isRecoverableStep9AuthFailure,
 } = self.MultiPageActivationUtils;
-const buildHotmailMailApiLatestUrl = buildHotmailGraphMessagesUrl;
-const getHotmailMailApiRequestConfig = getHotmailGraphRequestConfig;
 
 const LOG_PREFIX = '[MultiPage:bg]';
 const DUCK_AUTOFILL_URL = 'https://duckduckgo.com/email/settings/autofill';
@@ -40,6 +38,10 @@ const AUTO_RUN_ALARM_NAME = 'scheduled-auto-run';
 const AUTO_RUN_DELAY_MIN_MINUTES = 1;
 const AUTO_RUN_DELAY_MAX_MINUTES = 1440;
 const DEFAULT_LOCAL_CPA_STEP9_MODE = 'submit';
+const DEFAULT_HOTMAIL_MAIL_API_URL = 'https://apple.882263.xyz/api/mail-new';
+const DEFAULT_HOTMAIL_MAIL_API_RESPONSE_TYPE = 'json';
+const DEFAULT_HOTMAIL_MAIL_API_INBOX_MAILBOX = 'INBOX';
+const DEFAULT_HOTMAIL_MAIL_API_JUNK_MAILBOX = 'Junk';
 
 initializeSessionStorageAccess();
 
@@ -64,6 +66,10 @@ const PERSISTED_SETTING_DEFAULTS = {
   emailGenerator: 'duck', // 注册邮箱生成方式:duck / cloudflare。
   inbucketHost: '', // 仅当 mailProvider 为 inbucket 时填写 Inbucket 地址,其他情况保持为空。
   inbucketMailbox: '', // 仅当 mailProvider 为 inbucket 时填写邮箱名,其他情况保持为空。
+  hotmailApiUrl: DEFAULT_HOTMAIL_MAIL_API_URL, // Hotmail 第三方邮件 API 地址。
+  hotmailApiResponseType: DEFAULT_HOTMAIL_MAIL_API_RESPONSE_TYPE, // Hotmail 第三方邮件 API 的 response_type 参数。
+  hotmailApiInboxMailbox: DEFAULT_HOTMAIL_MAIL_API_INBOX_MAILBOX, // Hotmail API 收件箱参数值。
+  hotmailApiJunkMailbox: DEFAULT_HOTMAIL_MAIL_API_JUNK_MAILBOX, // Hotmail API 垃圾箱参数值。
   cloudflareDomain: '', // 仅当 emailGenerator=cloudflare 时填写自定义域名。
   cloudflareDomains: [], // Cloudflare 可选域名列表。
   hotmailAccounts: [],
@@ -191,6 +197,56 @@ function normalizeCloudflareDomains(values) {
   return normalizedDomains;
 }
 
+function normalizeHotmailMailApiUrl(rawValue = '') {
+  const value = String(rawValue || '').trim();
+  if (!value) return DEFAULT_HOTMAIL_MAIL_API_URL;
+
+  try {
+    const parsed = new URL(value);
+    if (!['http:', 'https:'].includes(parsed.protocol)) {
+      return DEFAULT_HOTMAIL_MAIL_API_URL;
+    }
+    return parsed.toString();
+  } catch {
+    return DEFAULT_HOTMAIL_MAIL_API_URL;
+  }
+}
+
+function normalizeHotmailMailApiResponseType(rawValue) {
+  if (rawValue === undefined || rawValue === null) {
+    return DEFAULT_HOTMAIL_MAIL_API_RESPONSE_TYPE;
+  }
+  return String(rawValue).trim();
+}
+
+function normalizeHotmailMailApiMailbox(rawValue, fallback) {
+  if (rawValue === undefined || rawValue === null) {
+    return fallback;
+  }
+  const value = String(rawValue).trim();
+  return value || fallback;
+}
+
+function getHotmailMailApiSettings(state = {}) {
+  return {
+    apiUrl: normalizeHotmailMailApiUrl(state.hotmailApiUrl),
+    responseType: normalizeHotmailMailApiResponseType(state.hotmailApiResponseType),
+    inboxMailbox: normalizeHotmailMailApiMailbox(state.hotmailApiInboxMailbox, DEFAULT_HOTMAIL_MAIL_API_INBOX_MAILBOX),
+    junkMailbox: normalizeHotmailMailApiMailbox(state.hotmailApiJunkMailbox, DEFAULT_HOTMAIL_MAIL_API_JUNK_MAILBOX),
+  };
+}
+
+function resolveHotmailMailApiMailbox(mailbox, settings = {}) {
+  const normalized = String(mailbox || '').trim().toLowerCase();
+  if (normalized === 'junk' || normalized === 'junk email' || normalized === 'junkemail') {
+    return normalizeHotmailMailApiMailbox(settings.junkMailbox, DEFAULT_HOTMAIL_MAIL_API_JUNK_MAILBOX);
+  }
+  if (normalized === 'inbox' || !normalized) {
+    return normalizeHotmailMailApiMailbox(settings.inboxMailbox, DEFAULT_HOTMAIL_MAIL_API_INBOX_MAILBOX);
+  }
+  return String(mailbox || '').trim();
+}
+
 function normalizePersistentSettingValue(key, value) {
   switch (key) {
     case 'panelMode':
@@ -224,6 +280,14 @@ function normalizePersistentSettingValue(key, value) {
       return String(value || '').trim();
     case 'inbucketMailbox':
       return String(value || '').trim();
+    case 'hotmailApiUrl':
+      return normalizeHotmailMailApiUrl(value);
+    case 'hotmailApiResponseType':
+      return normalizeHotmailMailApiResponseType(value);
+    case 'hotmailApiInboxMailbox':
+      return normalizeHotmailMailApiMailbox(value, DEFAULT_HOTMAIL_MAIL_API_INBOX_MAILBOX);
+    case 'hotmailApiJunkMailbox':
+      return normalizeHotmailMailApiMailbox(value, DEFAULT_HOTMAIL_MAIL_API_JUNK_MAILBOX);
     case 'cloudflareDomain':
       return normalizeCloudflareDomain(value);
     case 'cloudflareDomains':
@@ -449,16 +513,14 @@ function normalizeHotmailAccount(account = {}) {
   const normalizedLastAuthAt = Number.isFinite(Number(account.lastAuthAt)) ? Number(account.lastAuthAt) : 0;
   const normalizedStatus = String(
     account.status
-    || (normalizedLastAuthAt > 0 || account.accessToken ? 'authorized' : 'pending')
+    || (normalizedLastAuthAt > 0 ? 'authorized' : 'pending')
   );
   return {
     id: String(account.id || crypto.randomUUID()),
     email: String(account.email || '').trim(),
     password: String(account.password || ''),
     clientId: String(account.clientId || '').trim(),
-    accessToken: String(account.accessToken || ''),
     refreshToken: String(account.refreshToken || ''),
-    expiresAt: Number.isFinite(Number(account.expiresAt)) ? Number(account.expiresAt) : 0,
     status: normalizedStatus,
     enabled: account.enabled !== undefined ? Boolean(account.enabled) : true,
     used: Boolean(account.used),
@@ -513,8 +575,6 @@ async function upsertHotmailAccount(input) {
   const normalized = normalizeHotmailAccount({
     ...(existing || {}),
     ...(credentialsChanged ? {
-      accessToken: '',
-      expiresAt: 0,
       status: 'pending',
       lastAuthAt: 0,
       lastError: '',
@@ -650,7 +710,7 @@ async function ensureHotmailAccountForFlow(options = {}) {
   return setCurrentHotmailAccount(account.id, { markUsed, syncEmail: true });
 }
 
-async function requestHotmailMailApiLegacy(account, mailbox = 'INBOX') {
+async function requestHotmailMailApi(account, mailbox = 'INBOX') {
   if (!account?.email) {
     throw new Error('Hotmail 账号缺少邮箱地址。');
   }
@@ -661,12 +721,14 @@ async function requestHotmailMailApiLegacy(account, mailbox = 'INBOX') {
     throw new Error(`Hotmail 账号 ${account.email || account.id} 缺少刷新令牌(refresh token)。`);
   }
 
+  const hotmailApiSettings = getHotmailMailApiSettings(await getState());
   const url = buildHotmailMailApiLatestUrl({
+    apiUrl: hotmailApiSettings.apiUrl,
     clientId: account.clientId,
     email: account.email,
     refreshToken: account.refreshToken,
-    mailbox,
-    responseType: 'json',
+    mailbox: resolveHotmailMailApiMailbox(mailbox, hotmailApiSettings),
+    responseType: hotmailApiSettings.responseType,
   });
   const { timeoutMs } = getHotmailMailApiRequestConfig();
   const controller = new AbortController();
@@ -710,12 +772,10 @@ async function requestHotmailMailApiLegacy(account, mailbox = 'INBOX') {
   };
 }
 
-function applyHotmailApiResultToAccountLegacy(account, apiResult) {
+function applyHotmailApiResultToAccount(account, apiResult) {
   const nextRefreshToken = String(apiResult?.nextRefreshToken || '').trim();
   return {
     ...account,
-    accessToken: '',
-    expiresAt: 0,
     refreshToken: nextRefreshToken || account.refreshToken,
     status: 'authorized',
     lastAuthAt: Date.now(),
@@ -723,168 +783,9 @@ function applyHotmailApiResultToAccountLegacy(account, apiResult) {
   };
 }
 
-async function fetchHotmailMailboxMessagesLegacy(account, mailboxes = HOTMAIL_MAILBOXES) {
-  let workingAccount = normalizeHotmailAccount(account);
-  const mailboxResults = [];
-
-  for (const mailbox of mailboxes) {
-    const result = await requestHotmailMailApiLegacy(workingAccount, mailbox);
-    workingAccount = applyHotmailApiResultToAccountLegacy(workingAccount, result);
-    mailboxResults.push({
-      mailbox,
-      count: result.messages.length,
-      messages: result.messages.map((message) => ({ ...message, mailbox })),
-    });
-  }
-
-  const savedAccount = await upsertHotmailAccount(workingAccount);
-  return {
-    account: savedAccount,
-    mailboxResults,
-    messages: mailboxResults.flatMap((item) => item.messages),
-  };
-}
-
-function isHotmailAccessTokenUsable(account, now = Date.now()) {
-  return Boolean(account?.accessToken)
-    && Number(account?.expiresAt || 0) > now + 60_000;
-}
-
-async function refreshHotmailAccessToken(account) {
-  if (!account?.email) {
-    throw new Error('Hotmail 账号缺少邮箱地址。');
-  }
-  if (!account?.clientId) {
-    throw new Error(`Hotmail 账号 ${account.email || account.id} 缺少客户端 ID。`);
-  }
-  if (!account?.refreshToken) {
-    throw new Error(`Hotmail 账号 ${account.email || account.id} 缺少刷新令牌(refresh token)。`);
-  }
-
-  const { timeoutMs, scopes, tokenUrl } = getHotmailGraphRequestConfig();
-  const controller = new AbortController();
-  const timeoutId = setTimeout(() => controller.abort(new Error('timeout')), timeoutMs);
-  const formData = new URLSearchParams();
-  formData.set('client_id', account.clientId);
-  formData.set('grant_type', 'refresh_token');
-  formData.set('refresh_token', account.refreshToken);
-  formData.set('scope', scopes.join(' '));
-  formData.set('redirect_uri', 'https://login.microsoftonline.com/common/oauth2/nativeclient');
-
-  let response;
-  try {
-    response = await fetch(tokenUrl, {
-      method: 'POST',
-      headers: {
-        'Content-Type': 'application/x-www-form-urlencoded',
-      },
-      body: formData.toString(),
-      signal: controller.signal,
-    });
-  } catch (err) {
-    const error = new Error(
-      err?.name === 'AbortError'
-        ? `Hotmail 令牌刷新超时(>${Math.round(timeoutMs / 1000)} 秒)`
-        : `Hotmail 令牌刷新失败:${err.message}`
-    );
-    error.code = 'HOTMAIL_TOKEN_REFRESH_FAILED';
-    throw error;
-  } finally {
-    clearTimeout(timeoutId);
-  }
-
-  const text = await response.text();
-  let payload = {};
-  try {
-    payload = text ? JSON.parse(text) : {};
-  } catch {
-    payload = { raw: text };
-  }
-
-  if (!response.ok || !payload?.access_token) {
-    const rawErrorText = payload?.error_description || payload?.error?.message || payload?.error || payload?.message || text || `HTTP ${response.status}`;
-    const isCrossOriginError = typeof rawErrorText === 'string' && rawErrorText.includes('AADSTS90023');
-    const errorText = isCrossOriginError
-      ? `Azure AD 拒绝了跨域令牌请求(AADSTS90023)。请在 Azure AD 应用注册中将应用平台改为"单页应用程序(SPA)",并将重定向 URI 设置为 https://login.microsoftonline.com/common/oauth2/nativeclient,或将应用类型改为"移动和桌面应用程序(Native)"。`
-      : rawErrorText;
-    const error = new Error(`Hotmail 令牌刷新失败:${errorText}`);
-    error.code = 'HOTMAIL_TOKEN_REFRESH_FAILED';
-    throw error;
-  }
-
-  const expiresInSeconds = Math.max(60, Number(payload.expires_in || payload.expiresIn || 0) || 3600);
-  return normalizeHotmailAccount({
-    ...account,
-    accessToken: String(payload.access_token || ''),
-    refreshToken: String(payload.refresh_token || '').trim() || account.refreshToken,
-    expiresAt: Date.now() + expiresInSeconds * 1000,
-    status: 'authorized',
-    lastAuthAt: Date.now(),
-    lastError: '',
-  });
-}
-
-async function requestHotmailGraphMessages(account, mailbox = 'INBOX') {
-  const { timeoutMs, pageSize, messageFields } = getHotmailGraphRequestConfig();
-  const controller = new AbortController();
-  const timeoutId = setTimeout(() => controller.abort(new Error('timeout')), timeoutMs);
-  const url = buildHotmailGraphMessagesUrl({
-    mailbox,
-    top: pageSize,
-    selectFields: messageFields,
-  });
-
-  let response;
-  try {
-    response = await fetch(url, {
-      method: 'GET',
-      headers: {
-        Accept: 'application/json',
-        Authorization: `Bearer ${account.accessToken}`,
-      },
-      signal: controller.signal,
-    });
-  } catch (err) {
-    const error = new Error(
-      err?.name === 'AbortError'
-        ? `Hotmail 邮件请求超时(>${Math.round(timeoutMs / 1000)} 秒):${mailbox}`
-        : `Hotmail 邮件请求失败:${err.message}`
-    );
-    error.code = 'HOTMAIL_GRAPH_REQUEST_FAILED';
-    throw error;
-  } 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?.error?.message || payload?.error_description || payload?.message || text || `HTTP ${response.status}`;
-    const error = new Error(`Hotmail 邮件请求失败:${errorText}`);
-    error.code = response.status === 401 || response.status === 403
-      ? 'HOTMAIL_GRAPH_AUTH_FAILED'
-      : 'HOTMAIL_GRAPH_REQUEST_FAILED';
-    throw error;
-  }
-
-  return {
-    mailbox,
-    payload,
-    messages: normalizeHotmailMailApiMessages(payload?.value),
-  };
-}
-
-function buildHotmailAuthFailureAccount(account, errorMessage) {
+function buildHotmailMailApiFailureAccount(account, errorMessage) {
   return normalizeHotmailAccount({
     ...account,
-    accessToken: '',
-    expiresAt: 0,
     status: 'error',
     lastError: String(errorMessage || ''),
   });
@@ -895,27 +796,9 @@ async function fetchHotmailMailboxMessages(account, mailboxes = HOTMAIL_MAILBOXE
   const mailboxResults = [];
 
   try {
-    if (!isHotmailAccessTokenUsable(workingAccount)) {
-      workingAccount = await refreshHotmailAccessToken(workingAccount);
-    }
-
     for (const mailbox of mailboxes) {
-      let result;
-      try {
-        result = await requestHotmailGraphMessages(workingAccount, mailbox);
-      } catch (err) {
-        if (err?.code !== 'HOTMAIL_GRAPH_AUTH_FAILED') {
-          throw err;
-        }
-
-        workingAccount = await refreshHotmailAccessToken({
-          ...workingAccount,
-          accessToken: '',
-          expiresAt: 0,
-        });
-        result = await requestHotmailGraphMessages(workingAccount, mailbox);
-      }
-
+      const result = await requestHotmailMailApi(workingAccount, mailbox);
+      workingAccount = applyHotmailApiResultToAccount(workingAccount, result);
       mailboxResults.push({
         mailbox,
         count: result.messages.length,
@@ -923,10 +806,8 @@ async function fetchHotmailMailboxMessages(account, mailboxes = HOTMAIL_MAILBOXE
       });
     }
   } catch (err) {
-    if (err?.code === 'HOTMAIL_TOKEN_REFRESH_FAILED' || err?.code === 'HOTMAIL_GRAPH_AUTH_FAILED') {
-      const failedAccount = buildHotmailAuthFailureAccount(workingAccount, err.message);
-      await upsertHotmailAccount(failedAccount);
-    }
+    const failedAccount = buildHotmailMailApiFailureAccount(workingAccount, err.message);
+    await upsertHotmailAccount(failedAccount);
     throw err;
   }
 
@@ -1874,7 +1755,7 @@ function getSourceLabel(source) {
     'mail-163': '163 邮箱',
     'inbucket-mail': 'Inbucket 邮箱',
     'duck-mail': 'Duck 邮箱',
-    'hotmail-api': 'Hotmail(微软 Graph)',
+    'hotmail-api': 'Hotmail(第三方邮件 API)',
   };
   return labels[source] || source || '未知来源';
 }
@@ -2650,7 +2531,7 @@ async function handleMessage(message, sender) {
       const updates = buildPersistentSettingsPayload(message.payload || {});
       await setPersistentSettings(updates);
       await setState(updates);
-      return { ok: true };
+      return { ok: true, state: await getState() };
     }
 
     case 'EXPORT_SETTINGS': {
@@ -3738,7 +3619,7 @@ async function executeStep3(state) {
 function getMailConfig(state) {
   const provider = state.mailProvider || 'qq';
   if (provider === HOTMAIL_PROVIDER) {
-    return { provider: HOTMAIL_PROVIDER, label: 'Hotmail(微软 Graph)' };
+    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 邮箱' };

+ 16 - 37
hotmail-utils.js

@@ -6,22 +6,7 @@
 
   root.HotmailUtils = factory();
 })(typeof self !== 'undefined' ? self : globalThis, function createHotmailUtils() {
-  const HOTMAIL_MICROSOFT_TOKEN_URL = 'https://login.microsoftonline.com/consumers/oauth2/v2.0/token';
-  const HOTMAIL_GRAPH_API_ORIGIN = 'https://graph.microsoft.com';
-  const HOTMAIL_GRAPH_PAGE_SIZE = 10;
-  const HOTMAIL_GRAPH_MESSAGE_FIELDS = [
-    'id',
-    'internetMessageId',
-    'subject',
-    'from',
-    'bodyPreview',
-    'receivedDateTime',
-  ];
-  const HOTMAIL_GRAPH_SCOPES = [
-    'offline_access',
-    'https://graph.microsoft.com/Mail.Read',
-    'https://graph.microsoft.com/User.Read',
-  ];
+  const HOTMAIL_MAIL_API_URL = 'https://apple.882263.xyz/api/mail-new';
 
   function normalizeText(value) {
     return String(value || '')
@@ -291,20 +276,19 @@
     return list.map((message) => normalizeHotmailMailApiMessage(message));
   }
 
-  function normalizeHotmailMailboxId(mailbox = 'INBOX') {
-    const normalized = normalizeText(mailbox);
-    if (normalized === 'junk' || normalized === 'junk email' || normalized === 'junkemail') {
-      return 'junkemail';
+  function buildHotmailMailApiLatestUrl(options) {
+    const apiUrl = String(options?.apiUrl || '').trim() || HOTMAIL_MAIL_API_URL;
+    const url = new URL(apiUrl);
+    url.searchParams.set('refresh_token', String(options?.refreshToken || ''));
+    url.searchParams.set('client_id', String(options?.clientId || ''));
+    url.searchParams.set('email', String(options?.email || ''));
+    url.searchParams.set('mailbox', String(options?.mailbox || 'INBOX'));
+    const responseType = options?.responseType === undefined || options?.responseType === null
+      ? 'json'
+      : String(options.responseType).trim();
+    if (responseType) {
+      url.searchParams.set('response_type', responseType);
     }
-    return 'inbox';
-  }
-
-  function buildHotmailGraphMessagesUrl(options) {
-    const folderId = normalizeHotmailMailboxId(options?.mailbox);
-    const url = new URL(`${HOTMAIL_GRAPH_API_ORIGIN}/v1.0/me/mailFolders/${folderId}/messages`);
-    url.searchParams.set('$top', String(options?.top || HOTMAIL_GRAPH_PAGE_SIZE));
-    url.searchParams.set('$select', (options?.selectFields || HOTMAIL_GRAPH_MESSAGE_FIELDS).join(','));
-    url.searchParams.set('$orderby', String(options?.orderBy || 'receivedDateTime desc'));
     return url.toString();
   }
 
@@ -348,13 +332,9 @@
       : (flowStartTime || 0);
   }
 
-  function getHotmailGraphRequestConfig() {
+  function getHotmailMailApiRequestConfig() {
     return {
       timeoutMs: 15000,
-      pageSize: HOTMAIL_GRAPH_PAGE_SIZE,
-      scopes: HOTMAIL_GRAPH_SCOPES.slice(),
-      tokenUrl: HOTMAIL_MICROSOFT_TOKEN_URL,
-      messageFields: HOTMAIL_GRAPH_MESSAGE_FIELDS.slice(),
     };
   }
 
@@ -377,17 +357,16 @@
   }
 
   return {
-    buildHotmailGraphMessagesUrl,
+    buildHotmailMailApiLatestUrl,
     extractVerificationCodeFromMessage,
     filterHotmailAccountsByUsage,
     extractVerificationCode,
     getLatestHotmailMessage,
     getHotmailBulkActionLabel,
     getHotmailListToggleLabel,
-    getHotmailGraphRequestConfig,
+    getHotmailMailApiRequestConfig,
     getHotmailVerificationPollConfig,
     getHotmailVerificationRequestTimestamp,
-    normalizeHotmailMailboxId,
     isAuthorizedHotmailAccount,
     normalizeHotmailMailApiMessages,
     normalizeTimestamp,

+ 18 - 2
sidepanel/sidepanel.html

@@ -130,7 +130,7 @@
       <div class="data-row">
         <span class="data-label">邮箱服务</span>
         <select id="select-mail-provider" class="data-select">
-          <option value="hotmail-api">Hotmail(微软 Graph)</option>
+          <option value="hotmail-api">Hotmail(第三方邮件 API)</option>
           <option value="163">163 邮箱 (mail.163.com)</option>
           <option value="163-vip">163 VIP 邮箱 (webmail.vip.163.com)</option>
           <option value="qq">QQ 邮箱 (wx.mail.qq.com)</option>
@@ -205,7 +205,7 @@
       <div class="section-mini-header">
         <div class="section-mini-copy">
           <span class="section-label">Hotmail 账号池</span>
-          <span class="section-hint">每轮自动分配一个可用账号</span>
+          <span class="section-hint">先配置 API,再维护账号池并校验</span>
         </div>
         <div class="section-mini-actions">
           <button id="btn-clear-used-hotmail-accounts" class="btn btn-ghost btn-xs" type="button">清空已用</button>
@@ -213,6 +213,22 @@
           <button id="btn-toggle-hotmail-list" class="btn btn-ghost btn-xs" type="button" aria-expanded="false">展开列表</button>
         </div>
       </div>
+      <div class="data-row">
+        <span class="data-label">API 地址</span>
+        <input type="text" id="input-hotmail-api-url" class="data-input mono" placeholder="https://apple.882263.xyz/api/mail-new" />
+      </div>
+      <div class="data-row">
+        <span class="data-label">响应类型</span>
+        <input type="text" id="input-hotmail-api-response-type" class="data-input mono" placeholder="json;留空则不传 response_type" />
+      </div>
+      <div class="data-row">
+        <span class="data-label">收件箱参数</span>
+        <input type="text" id="input-hotmail-api-inbox-mailbox" class="data-input mono" placeholder="INBOX" />
+      </div>
+      <div class="data-row">
+        <span class="data-label">垃圾箱参数</span>
+        <input type="text" id="input-hotmail-api-junk-mailbox" class="data-input mono" placeholder="Junk" />
+      </div>
       <div class="data-row">
         <span class="data-label">邮箱</span>
         <input type="text" id="input-hotmail-email" class="data-input" placeholder="name@hotmail.com" />

+ 31 - 5
sidepanel/sidepanel.js

@@ -60,6 +60,10 @@ const selectMailProvider = document.getElementById('select-mail-provider');
 const rowEmailGenerator = document.getElementById('row-email-generator');
 const selectEmailGenerator = document.getElementById('select-email-generator');
 const hotmailSection = document.getElementById('hotmail-section');
+const inputHotmailApiUrl = document.getElementById('input-hotmail-api-url');
+const inputHotmailApiResponseType = document.getElementById('input-hotmail-api-response-type');
+const inputHotmailApiInboxMailbox = document.getElementById('input-hotmail-api-inbox-mailbox');
+const inputHotmailApiJunkMailbox = document.getElementById('input-hotmail-api-junk-mailbox');
 const inputHotmailEmail = document.getElementById('input-hotmail-email');
 const inputHotmailClientId = document.getElementById('input-hotmail-client-id');
 const inputHotmailPassword = document.getElementById('input-hotmail-password');
@@ -597,6 +601,10 @@ function collectSettingsPayload() {
     emailGenerator: selectEmailGenerator.value,
     inbucketHost: inputInbucketHost.value.trim(),
     inbucketMailbox: inputInbucketMailbox.value.trim(),
+    hotmailApiUrl: inputHotmailApiUrl.value.trim(),
+    hotmailApiResponseType: inputHotmailApiResponseType.value.trim(),
+    hotmailApiInboxMailbox: inputHotmailApiInboxMailbox.value.trim(),
+    hotmailApiJunkMailbox: inputHotmailApiJunkMailbox.value.trim(),
     cloudflareDomain: selectedCloudflareDomain,
     cloudflareDomains: domains,
     autoRunSkipFailures: inputAutoSkipFailures.checked,
@@ -708,11 +716,15 @@ async function saveSettings(options = {}) {
       throw new Error(response.error);
     }
 
-    syncLatestState(payload);
-    markSettingsDirty(false);
-    updatePanelModeUI();
-    updateMailProviderUI();
-    updateButtonStates();
+    if (response?.state) {
+      applySettingsState(response.state);
+    } else {
+      syncLatestState(payload);
+      markSettingsDirty(false);
+      updatePanelModeUI();
+      updateMailProviderUI();
+      updateButtonStates();
+    }
     if (!silent) {
       showToast('配置已保存', 'success', 1800);
     }
@@ -838,6 +850,10 @@ function applySettingsState(state) {
   selectEmailGenerator.value = state?.emailGenerator || 'duck';
   inputInbucketHost.value = state?.inbucketHost || '';
   inputInbucketMailbox.value = state?.inbucketMailbox || '';
+  inputHotmailApiUrl.value = state?.hotmailApiUrl || '';
+  inputHotmailApiResponseType.value = state?.hotmailApiResponseType ?? '';
+  inputHotmailApiInboxMailbox.value = state?.hotmailApiInboxMailbox || '';
+  inputHotmailApiJunkMailbox.value = state?.hotmailApiJunkMailbox || '';
   renderCloudflareDomainOptions(state?.cloudflareDomain || '');
   setCloudflareDomainEditMode(false, { clearInput: true });
   inputAutoSkipFailures.checked = Boolean(state?.autoRunSkipFailures);
@@ -2200,6 +2216,16 @@ inputVpsPassword.addEventListener('blur', () => {
   saveSettings({ silent: true }).catch(() => { });
 });
 
+[inputHotmailApiUrl, inputHotmailApiResponseType, inputHotmailApiInboxMailbox, inputHotmailApiJunkMailbox].forEach((input) => {
+  input?.addEventListener('input', () => {
+    markSettingsDirty(true);
+    scheduleSettingsAutoSave();
+  });
+  input?.addEventListener('blur', () => {
+    saveSettings({ silent: true }).catch(() => { });
+  });
+});
+
 inputPassword.addEventListener('input', () => {
   markSettingsDirty(true);
   updateButtonStates();

+ 31 - 31
tests/hotmail-utils.test.js

@@ -2,17 +2,16 @@ const test = require('node:test');
 const assert = require('node:assert/strict');
 
 const {
-  buildHotmailGraphMessagesUrl,
+  buildHotmailMailApiLatestUrl,
   extractVerificationCodeFromMessage,
   filterHotmailAccountsByUsage,
   extractVerificationCode,
   getLatestHotmailMessage,
   getHotmailBulkActionLabel,
   getHotmailListToggleLabel,
-  getHotmailGraphRequestConfig,
+  getHotmailMailApiRequestConfig,
   getHotmailVerificationPollConfig,
   getHotmailVerificationRequestTimestamp,
-  normalizeHotmailMailboxId,
   normalizeHotmailMailApiMessages,
   parseHotmailImportText,
   pickHotmailAccountForRun,
@@ -346,22 +345,38 @@ test('pickVerificationMessageWithTimeFallback can ignore afterTimestamp while ke
   assert.equal(result.usedTimeFallback, true);
 });
 
-test('buildHotmailGraphMessagesUrl targets the official Microsoft Graph mailbox endpoint', () => {
-  const url = new URL(buildHotmailGraphMessagesUrl({
+test('buildHotmailMailApiLatestUrl includes email, client id, refresh token, and mailbox', () => {
+  const url = new URL(buildHotmailMailApiLatestUrl({
+    clientId: 'client-123',
+    email: 'user@hotmail.com',
+    refreshToken: 'refresh-token-xyz',
     mailbox: 'Junk',
   }));
 
-  assert.equal(url.origin + url.pathname, 'https://graph.microsoft.com/v1.0/me/mailFolders/junkemail/messages');
-  assert.equal(url.searchParams.get('$top'), '10');
-  assert.equal(url.searchParams.get('$orderby'), 'receivedDateTime desc');
-  assert.match(url.searchParams.get('$select'), /subject/);
-  assert.match(url.searchParams.get('$select'), /receivedDateTime/);
+  assert.equal(url.origin + url.pathname, 'https://apple.882263.xyz/api/mail-new');
+  assert.equal(url.searchParams.get('client_id'), 'client-123');
+  assert.equal(url.searchParams.get('email'), 'user@hotmail.com');
+  assert.equal(url.searchParams.get('refresh_token'), 'refresh-token-xyz');
+  assert.equal(url.searchParams.get('mailbox'), 'Junk');
+  assert.equal(url.searchParams.get('response_type'), 'json');
 });
 
-test('normalizeHotmailMailboxId maps supported mailbox labels to Graph folder ids', () => {
-  assert.equal(normalizeHotmailMailboxId('INBOX'), 'inbox');
-  assert.equal(normalizeHotmailMailboxId('Junk'), 'junkemail');
-  assert.equal(normalizeHotmailMailboxId('junkemail'), 'junkemail');
+test('buildHotmailMailApiLatestUrl supports custom api url and can omit response_type', () => {
+  const url = new URL(buildHotmailMailApiLatestUrl({
+    apiUrl: 'https://example.com/custom-mail-api',
+    clientId: 'client-789',
+    email: 'custom@hotmail.com',
+    refreshToken: 'refresh-token-custom',
+    mailbox: 'Spam',
+    responseType: '',
+  }));
+
+  assert.equal(url.origin + url.pathname, 'https://example.com/custom-mail-api');
+  assert.equal(url.searchParams.get('client_id'), 'client-789');
+  assert.equal(url.searchParams.get('email'), 'custom@hotmail.com');
+  assert.equal(url.searchParams.get('refresh_token'), 'refresh-token-custom');
+  assert.equal(url.searchParams.get('mailbox'), 'Spam');
+  assert.equal(url.searchParams.has('response_type'), false);
 });
 
 test('normalizeHotmailMailApiMessages maps third-party payload fields into verification message shape', () => {
@@ -440,24 +455,9 @@ test('getHotmailVerificationRequestTimestamp prefers actual request timestamps w
   );
 });
 
-test('getHotmailGraphRequestConfig defines Microsoft Graph request defaults', () => {
-  assert.deepEqual(getHotmailGraphRequestConfig(), {
+test('getHotmailMailApiRequestConfig defines third-party mail API request defaults', () => {
+  assert.deepEqual(getHotmailMailApiRequestConfig(), {
     timeoutMs: 15000,
-    pageSize: 10,
-    scopes: [
-      'offline_access',
-      'https://graph.microsoft.com/Mail.Read',
-      'https://graph.microsoft.com/User.Read',
-    ],
-    tokenUrl: 'https://login.microsoftonline.com/consumers/oauth2/v2.0/token',
-    messageFields: [
-      'id',
-      'internetMessageId',
-      'subject',
-      'from',
-      'bodyPreview',
-      'receivedDateTime',
-    ],
   });
 });