Merge branch 'hotmail邮箱支持' into master
This commit is contained in:
+290
-175
@@ -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';
|
||||
@@ -43,6 +41,11 @@ const AUTO_STEP_DELAY_MIN_ALLOWED_SECONDS = 0;
|
||||
const AUTO_STEP_DELAY_MAX_ALLOWED_SECONDS = 600;
|
||||
const LEGACY_AUTO_STEP_DELAY_KEYS = ['autoStepRandomDelayMinSeconds', 'autoStepRandomDelayMaxSeconds'];
|
||||
const DEFAULT_LOCAL_CPA_STEP9_MODE = 'submit';
|
||||
const HOTMAIL_SERVICE_MODE_REMOTE = 'remote';
|
||||
const HOTMAIL_SERVICE_MODE_LOCAL = 'local';
|
||||
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;
|
||||
|
||||
initializeSessionStorageAccess();
|
||||
|
||||
@@ -70,6 +73,9 @@ const PERSISTED_SETTING_DEFAULTS = {
|
||||
emailPrefix: '',
|
||||
inbucketHost: '',
|
||||
inbucketMailbox: '',
|
||||
hotmailServiceMode: HOTMAIL_SERVICE_MODE_LOCAL,
|
||||
hotmailRemoteBaseUrl: DEFAULT_HOTMAIL_REMOTE_BASE_URL,
|
||||
hotmailLocalBaseUrl: DEFAULT_HOTMAIL_LOCAL_BASE_URL,
|
||||
cloudflareDomain: '',
|
||||
cloudflareDomains: [],
|
||||
hotmailAccounts: [],
|
||||
@@ -253,6 +259,62 @@ 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;
|
||||
|
||||
try {
|
||||
const parsed = new URL(value);
|
||||
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
||||
return DEFAULT_HOTMAIL_REMOTE_BASE_URL;
|
||||
}
|
||||
|
||||
if (parsed.pathname.endsWith('/api/mail-new') || parsed.pathname.endsWith('/api/mail-all') || parsed.pathname === '/api.html') {
|
||||
parsed.pathname = '';
|
||||
parsed.search = '';
|
||||
parsed.hash = '';
|
||||
}
|
||||
|
||||
return parsed.toString().replace(/\/$/, '');
|
||||
} catch {
|
||||
return DEFAULT_HOTMAIL_REMOTE_BASE_URL;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeHotmailLocalBaseUrl(rawValue = '') {
|
||||
const value = String(rawValue || '').trim();
|
||||
if (!value) return DEFAULT_HOTMAIL_LOCAL_BASE_URL;
|
||||
|
||||
try {
|
||||
const parsed = new URL(value);
|
||||
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
||||
return DEFAULT_HOTMAIL_LOCAL_BASE_URL;
|
||||
}
|
||||
|
||||
if (['/messages', '/code', '/clear', '/token'].includes(parsed.pathname)) {
|
||||
parsed.pathname = '';
|
||||
parsed.search = '';
|
||||
parsed.hash = '';
|
||||
}
|
||||
|
||||
return parsed.toString().replace(/\/$/, '');
|
||||
} catch {
|
||||
return DEFAULT_HOTMAIL_LOCAL_BASE_URL;
|
||||
}
|
||||
}
|
||||
|
||||
function getHotmailServiceSettings(state = {}) {
|
||||
return {
|
||||
mode: normalizeHotmailServiceMode(state.hotmailServiceMode),
|
||||
remoteBaseUrl: normalizeHotmailRemoteBaseUrl(state.hotmailRemoteBaseUrl),
|
||||
localBaseUrl: normalizeHotmailLocalBaseUrl(state.hotmailLocalBaseUrl),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePersistentSettingValue(key, value) {
|
||||
switch (key) {
|
||||
case 'panelMode':
|
||||
@@ -292,6 +354,12 @@ function normalizePersistentSettingValue(key, value) {
|
||||
return String(value || '').trim();
|
||||
case 'inbucketMailbox':
|
||||
return String(value || '').trim();
|
||||
case 'hotmailServiceMode':
|
||||
return normalizeHotmailServiceMode(value);
|
||||
case 'hotmailRemoteBaseUrl':
|
||||
return normalizeHotmailRemoteBaseUrl(value);
|
||||
case 'hotmailLocalBaseUrl':
|
||||
return normalizeHotmailLocalBaseUrl(value);
|
||||
case 'cloudflareDomain':
|
||||
return normalizeCloudflareDomain(value);
|
||||
case 'cloudflareDomains':
|
||||
@@ -523,16 +591,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),
|
||||
@@ -587,8 +653,6 @@ async function upsertHotmailAccount(input) {
|
||||
const normalized = normalizeHotmailAccount({
|
||||
...(existing || {}),
|
||||
...(credentialsChanged ? {
|
||||
accessToken: '',
|
||||
expiresAt: 0,
|
||||
status: 'pending',
|
||||
lastAuthAt: 0,
|
||||
lastError: '',
|
||||
@@ -724,7 +788,17 @@ async function ensureHotmailAccountForFlow(options = {}) {
|
||||
return setCurrentHotmailAccount(account.id, { markUsed, syncEmail: true });
|
||||
}
|
||||
|
||||
async function requestHotmailMailApiLegacy(account, mailbox = 'INBOX') {
|
||||
function buildHotmailRemoteEndpoint(baseUrl, path) {
|
||||
const normalizedBaseUrl = normalizeHotmailRemoteBaseUrl(baseUrl);
|
||||
return new URL(path, `${normalizedBaseUrl}/`).toString();
|
||||
}
|
||||
|
||||
function buildHotmailLocalEndpoint(baseUrl, path) {
|
||||
const normalizedBaseUrl = normalizeHotmailLocalBaseUrl(baseUrl);
|
||||
return new URL(path, `${normalizedBaseUrl}/`).toString();
|
||||
}
|
||||
|
||||
async function requestHotmailRemoteMailbox(account, mailbox = 'INBOX') {
|
||||
if (!account?.email) {
|
||||
throw new Error('Hotmail 账号缺少邮箱地址。');
|
||||
}
|
||||
@@ -735,7 +809,9 @@ async function requestHotmailMailApiLegacy(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,
|
||||
@@ -784,12 +860,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(),
|
||||
@@ -797,18 +871,32 @@ function applyHotmailApiResultToAccountLegacy(account, apiResult) {
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchHotmailMailboxMessagesLegacy(account, mailboxes = HOTMAIL_MAILBOXES) {
|
||||
function buildHotmailMailApiFailureAccount(account, errorMessage) {
|
||||
return normalizeHotmailAccount({
|
||||
...account,
|
||||
status: 'error',
|
||||
lastError: String(errorMessage || ''),
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchHotmailMailboxMessagesFromRemoteService(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 })),
|
||||
});
|
||||
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 })),
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
const failedAccount = buildHotmailMailApiFailureAccount(workingAccount, err.message);
|
||||
await upsertHotmailAccount(failedAccount);
|
||||
throw err;
|
||||
}
|
||||
|
||||
const savedAccount = await upsertHotmailAccount(workingAccount);
|
||||
@@ -819,12 +907,7 @@ async function fetchHotmailMailboxMessagesLegacy(account, mailboxes = HOTMAIL_MA
|
||||
};
|
||||
}
|
||||
|
||||
function isHotmailAccessTokenUsable(account, now = Date.now()) {
|
||||
return Boolean(account?.accessToken)
|
||||
&& Number(account?.expiresAt || 0) > now + 60_000;
|
||||
}
|
||||
|
||||
async function refreshHotmailAccessToken(account) {
|
||||
async function requestHotmailLocalMessages(account, mailboxes = HOTMAIL_MAILBOXES) {
|
||||
if (!account?.email) {
|
||||
throw new Error('Hotmail 账号缺少邮箱地址。');
|
||||
}
|
||||
@@ -835,97 +918,34 @@ async function refreshHotmailAccessToken(account) {
|
||||
throw new Error(`Hotmail 账号 ${account.email || account.id} 缺少刷新令牌(refresh token)。`);
|
||||
}
|
||||
|
||||
const { timeoutMs, scopes, tokenUrl } = getHotmailGraphRequestConfig();
|
||||
const serviceSettings = getHotmailServiceSettings(await getState());
|
||||
const { timeoutMs } = getHotmailMailApiRequestConfig();
|
||||
const requestTimeoutMs = Math.max(timeoutMs, HOTMAIL_LOCAL_HELPER_TIMEOUT_MS);
|
||||
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');
|
||||
const timeoutId = setTimeout(() => controller.abort(new Error('timeout')), requestTimeoutMs);
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(tokenUrl, {
|
||||
response = await fetch(buildHotmailLocalEndpoint(serviceSettings.localBaseUrl, '/messages'), {
|
||||
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: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${account.accessToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: account.email,
|
||||
clientId: account.clientId,
|
||||
refreshToken: account.refreshToken,
|
||||
mailboxes,
|
||||
top: 5,
|
||||
}),
|
||||
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;
|
||||
if (err?.name === 'AbortError') {
|
||||
throw new Error(`Hotmail 本地助手请求超时(>${Math.round(requestTimeoutMs / 1000)} 秒)`);
|
||||
}
|
||||
throw new Error(`Hotmail 本地助手请求失败:${err.message}`);
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
@@ -938,80 +958,170 @@ async function requestHotmailGraphMessages(account, mailbox = 'INBOX') {
|
||||
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;
|
||||
if (!response.ok || payload?.ok === false) {
|
||||
const errorText = payload?.error || payload?.message || text || `HTTP ${response.status}`;
|
||||
throw new Error(`Hotmail 本地助手返回失败:${errorText}`);
|
||||
}
|
||||
|
||||
return {
|
||||
mailbox,
|
||||
payload,
|
||||
messages: normalizeHotmailMailApiMessages(payload?.value),
|
||||
};
|
||||
}
|
||||
const rawMessages = Array.isArray(payload?.messages) ? payload.messages : [];
|
||||
const normalizedMessages = normalizeHotmailMailApiMessages(rawMessages).map((message, index) => ({
|
||||
...message,
|
||||
mailbox: rawMessages[index]?.mailbox || 'INBOX',
|
||||
receivedTimestamp: Number(rawMessages[index]?.receivedTimestamp || 0) || 0,
|
||||
}));
|
||||
const mailboxResults = Array.isArray(payload?.mailboxResults)
|
||||
? payload.mailboxResults.map((item) => ({
|
||||
mailbox: String(item?.mailbox || 'INBOX'),
|
||||
count: Number(item?.count || 0),
|
||||
messages: normalizedMessages.filter((message) => String(message.mailbox || 'INBOX') === String(item?.mailbox || 'INBOX')),
|
||||
}))
|
||||
: mailboxes.map((mailbox) => ({
|
||||
mailbox,
|
||||
count: normalizedMessages.filter((message) => String(message.mailbox || 'INBOX') === mailbox).length,
|
||||
messages: normalizedMessages.filter((message) => String(message.mailbox || 'INBOX') === mailbox),
|
||||
}));
|
||||
|
||||
function buildHotmailAuthFailureAccount(account, errorMessage) {
|
||||
return normalizeHotmailAccount({
|
||||
...account,
|
||||
accessToken: '',
|
||||
expiresAt: 0,
|
||||
status: 'error',
|
||||
lastError: String(errorMessage || ''),
|
||||
const nextAccount = applyHotmailApiResultToAccount(account, {
|
||||
nextRefreshToken: String(payload?.nextRefreshToken || '').trim(),
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchHotmailMailboxMessages(account, mailboxes = HOTMAIL_MAILBOXES) {
|
||||
let workingAccount = normalizeHotmailAccount(account);
|
||||
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);
|
||||
}
|
||||
|
||||
mailboxResults.push({
|
||||
mailbox,
|
||||
count: result.messages.length,
|
||||
messages: result.messages.map((message) => ({ ...message, mailbox })),
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
if (err?.code === 'HOTMAIL_TOKEN_REFRESH_FAILED' || err?.code === 'HOTMAIL_GRAPH_AUTH_FAILED') {
|
||||
const failedAccount = buildHotmailAuthFailureAccount(workingAccount, err.message);
|
||||
await upsertHotmailAccount(failedAccount);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
const savedAccount = await upsertHotmailAccount(workingAccount);
|
||||
const savedAccount = await upsertHotmailAccount(nextAccount);
|
||||
return {
|
||||
account: savedAccount,
|
||||
mailboxResults,
|
||||
messages: mailboxResults.flatMap((item) => item.messages),
|
||||
messages: normalizedMessages,
|
||||
};
|
||||
}
|
||||
|
||||
async function requestHotmailLocalCode(account, pollPayload = {}) {
|
||||
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 serviceSettings = getHotmailServiceSettings(await getState());
|
||||
const { timeoutMs } = getHotmailMailApiRequestConfig();
|
||||
const requestTimeoutMs = Math.max(timeoutMs, HOTMAIL_LOCAL_HELPER_TIMEOUT_MS);
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(new Error('timeout')), requestTimeoutMs);
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(buildHotmailLocalEndpoint(serviceSettings.localBaseUrl, '/code'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: account.email,
|
||||
clientId: account.clientId,
|
||||
refreshToken: account.refreshToken,
|
||||
mailboxes: HOTMAIL_MAILBOXES,
|
||||
top: 5,
|
||||
senderFilters: pollPayload.senderFilters || [],
|
||||
subjectFilters: pollPayload.subjectFilters || [],
|
||||
excludeCodes: pollPayload.excludeCodes || [],
|
||||
filterAfterTimestamp: Number(pollPayload.filterAfterTimestamp || 0) || 0,
|
||||
}),
|
||||
signal: controller.signal,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err?.name === 'AbortError') {
|
||||
throw new Error(`Hotmail 本地助手请求超时(>${Math.round(requestTimeoutMs / 1000)} 秒)`);
|
||||
}
|
||||
throw new Error(`Hotmail 本地助手请求失败:${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 || payload?.ok === false) {
|
||||
const errorText = payload?.error || payload?.message || text || `HTTP ${response.status}`;
|
||||
throw new Error(`Hotmail 本地助手返回失败:${errorText}`);
|
||||
}
|
||||
|
||||
const normalizedMessage = payload?.message
|
||||
? {
|
||||
...normalizeHotmailMailApiMessages([payload.message])[0],
|
||||
mailbox: payload?.message?.mailbox || 'INBOX',
|
||||
receivedTimestamp: Number(payload?.message?.receivedTimestamp || 0) || 0,
|
||||
}
|
||||
: null;
|
||||
const nextAccount = applyHotmailApiResultToAccount(account, {
|
||||
nextRefreshToken: String(payload?.nextRefreshToken || '').trim(),
|
||||
});
|
||||
const savedAccount = await upsertHotmailAccount(nextAccount);
|
||||
return {
|
||||
account: savedAccount,
|
||||
code: String(payload?.code || ''),
|
||||
message: normalizedMessage,
|
||||
usedTimeFallback: Boolean(payload?.usedTimeFallback),
|
||||
selectionSource: String(payload?.selectionSource || ''),
|
||||
};
|
||||
}
|
||||
|
||||
async function pollHotmailVerificationCodeViaLocalHelper(step, account, pollPayload = {}) {
|
||||
const maxAttempts = Number(pollPayload.maxAttempts) || 5;
|
||||
const intervalMs = Number(pollPayload.intervalMs) || 3000;
|
||||
let workingAccount = account;
|
||||
let lastError = null;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
throwIfStopped();
|
||||
try {
|
||||
await addLog(`步骤 ${step}:正在通过本地助手轮询 Hotmail 验证码(${attempt}/${maxAttempts})...`, 'info');
|
||||
const fetchResult = await requestHotmailLocalCode(workingAccount, pollPayload);
|
||||
workingAccount = fetchResult.account;
|
||||
|
||||
if (fetchResult.code) {
|
||||
const mailboxLabel = fetchResult.message?.mailbox || 'INBOX';
|
||||
if (fetchResult.usedTimeFallback) {
|
||||
await addLog(`步骤 ${step}:本地助手使用时间回退后命中 Hotmail ${mailboxLabel} 验证码。`, 'warn');
|
||||
}
|
||||
await addLog(`步骤 ${step}:已通过本地助手在 Hotmail ${mailboxLabel} 中找到验证码:${fetchResult.code}`, 'ok');
|
||||
return {
|
||||
ok: true,
|
||||
code: fetchResult.code,
|
||||
emailTimestamp: fetchResult.message?.receivedTimestamp || Date.now(),
|
||||
mailId: fetchResult.message?.id || '',
|
||||
};
|
||||
}
|
||||
|
||||
lastError = new Error(`步骤 ${step}:本地助手暂未返回匹配验证码(${attempt}/${maxAttempts})。`);
|
||||
await addLog(lastError.message, attempt === maxAttempts ? 'warn' : 'info');
|
||||
} catch (err) {
|
||||
lastError = err;
|
||||
await addLog(`步骤 ${step}:本地助手轮询 Hotmail 失败:${err.message}`, 'warn');
|
||||
}
|
||||
|
||||
if (attempt < maxAttempts) {
|
||||
await sleepWithStop(intervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError || new Error(`步骤 ${step}:本地助手未返回新的匹配验证码。`);
|
||||
}
|
||||
|
||||
async function fetchHotmailMailboxMessages(account, mailboxes = HOTMAIL_MAILBOXES) {
|
||||
const serviceSettings = getHotmailServiceSettings(await getState());
|
||||
if (serviceSettings.mode === HOTMAIL_SERVICE_MODE_LOCAL) {
|
||||
return requestHotmailLocalMessages(account, mailboxes);
|
||||
}
|
||||
return fetchHotmailMailboxMessagesFromRemoteService(account, mailboxes);
|
||||
}
|
||||
|
||||
async function verifyHotmailAccount(accountId) {
|
||||
const state = await getState();
|
||||
const account = findHotmailAccount(state.hotmailAccounts, accountId);
|
||||
@@ -1059,6 +1169,11 @@ async function pollHotmailVerificationCode(step, state, pollPayload = {}) {
|
||||
});
|
||||
await addLog(`步骤 ${step}:当前使用 Hotmail 账号 ${account.email} 轮询收件箱。`, 'info');
|
||||
|
||||
const serviceSettings = getHotmailServiceSettings(state);
|
||||
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;
|
||||
@@ -1979,7 +2094,7 @@ function getSourceLabel(source) {
|
||||
'mail-2925': '2925 邮箱',
|
||||
'inbucket-mail': 'Inbucket 邮箱',
|
||||
'duck-mail': 'Duck 邮箱',
|
||||
'hotmail-api': 'Hotmail(微软 Graph)',
|
||||
'hotmail-api': 'Hotmail(远程/本地)',
|
||||
};
|
||||
return labels[source] || source || '未知来源';
|
||||
}
|
||||
@@ -2759,7 +2874,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': {
|
||||
@@ -3881,7 +3996,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(远程/本地)' };
|
||||
}
|
||||
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 邮箱' };
|
||||
|
||||
Reference in New Issue
Block a user