feat: 新增 YYDS Mail 邮箱服务端点
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
(function yydsMailProviderModule(root, factory) {
|
||||
root.MultiPageBackgroundYydsMailProvider = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createYydsMailProviderModule() {
|
||||
function createYydsMailProvider(deps = {}) {
|
||||
const {
|
||||
addLog = async () => {},
|
||||
buildYydsMailHeaders,
|
||||
DEFAULT_YYDS_MAIL_BASE_URL = 'https://maliapi.215.im/v1',
|
||||
fetchImpl = typeof fetch === 'function' ? fetch.bind(globalThis) : null,
|
||||
getState = async () => ({}),
|
||||
joinYydsMailUrl,
|
||||
normalizeYydsMailAddress,
|
||||
normalizeYydsMailApiKey,
|
||||
normalizeYydsMailBaseUrl,
|
||||
normalizeYydsMailCurrentInbox,
|
||||
normalizeYydsMailInbox,
|
||||
normalizeYydsMailMessageDetail,
|
||||
normalizeYydsMailMessages,
|
||||
persistRegistrationEmailState = null,
|
||||
pickVerificationMessageWithTimeFallback,
|
||||
setEmailState = async () => {},
|
||||
setState = async () => {},
|
||||
sleepWithStop = async () => {},
|
||||
throwIfStopped = () => {},
|
||||
YYDS_MAIL_PROVIDER = 'yyds-mail',
|
||||
} = deps;
|
||||
|
||||
async function persistResolvedEmailState(state = null, email, options = {}) {
|
||||
if (typeof persistRegistrationEmailState === 'function') {
|
||||
await persistRegistrationEmailState(state, email, options);
|
||||
return;
|
||||
}
|
||||
await setEmailState(email, options);
|
||||
}
|
||||
|
||||
function getYydsMailConfig(state = {}) {
|
||||
return {
|
||||
apiKey: normalizeYydsMailApiKey(state.yydsMailApiKey),
|
||||
baseUrl: normalizeYydsMailBaseUrl(state.yydsMailBaseUrl || DEFAULT_YYDS_MAIL_BASE_URL),
|
||||
currentInbox: normalizeYydsMailCurrentInbox(state.currentYydsMailInbox),
|
||||
};
|
||||
}
|
||||
|
||||
function ensureYydsMailConfig(state = {}, options = {}) {
|
||||
const { requireApiKey = false, requireInbox = false } = options;
|
||||
const config = getYydsMailConfig(state);
|
||||
if (!config.baseUrl) {
|
||||
throw new Error('YYDS Mail API 地址为空或格式无效。');
|
||||
}
|
||||
if (requireApiKey && !config.apiKey) {
|
||||
throw new Error('YYDS Mail API Key 为空,请先在侧边栏填写。');
|
||||
}
|
||||
if (requireInbox && (!config.currentInbox?.address || !config.currentInbox?.token)) {
|
||||
throw new Error('YYDS Mail 当前没有可用邮箱,请先获取邮箱。');
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
async function requestYydsMailJson(config, path, options = {}) {
|
||||
if (!fetchImpl) {
|
||||
throw new Error('YYDS Mail 当前运行环境不支持 fetch。');
|
||||
}
|
||||
const {
|
||||
method = 'GET',
|
||||
payload,
|
||||
params,
|
||||
timeoutMs = 20000,
|
||||
auth = 'temp',
|
||||
} = options;
|
||||
const url = joinYydsMailUrl(config.baseUrl, path, params);
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(new Error('timeout')), timeoutMs);
|
||||
let response;
|
||||
try {
|
||||
response = await fetchImpl(url, {
|
||||
method,
|
||||
headers: buildYydsMailHeaders(config, {
|
||||
apiKey: auth === 'apiKey' ? config.apiKey : '',
|
||||
tempToken: auth === 'temp' ? config.currentInbox?.token : '',
|
||||
includeConfigApiKey: auth === 'apiKey',
|
||||
json: payload !== undefined,
|
||||
}),
|
||||
body: payload !== undefined ? JSON.stringify(payload || {}) : undefined,
|
||||
signal: controller.signal,
|
||||
});
|
||||
} catch (err) {
|
||||
const errorMessage = err?.name === 'AbortError'
|
||||
? `YYDS Mail 请求超时(>${Math.round(timeoutMs / 1000)} 秒)`
|
||||
: `YYDS Mail 请求失败:${err.message}`;
|
||||
throw new Error(errorMessage);
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
let parsed = {};
|
||||
try {
|
||||
parsed = text ? JSON.parse(text) : {};
|
||||
} catch {
|
||||
parsed = text;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const payloadError = parsed && typeof parsed === 'object'
|
||||
? (parsed.error || parsed.message || parsed.msg || parsed.errorCode)
|
||||
: '';
|
||||
throw new Error(`YYDS Mail 请求失败:${payloadError || text || `HTTP ${response.status}`}`);
|
||||
}
|
||||
|
||||
if (parsed && typeof parsed === 'object' && parsed.success === false) {
|
||||
throw new Error(`YYDS Mail 业务错误:${parsed.error || parsed.message || parsed.errorCode || 'unknown_error'}`);
|
||||
}
|
||||
|
||||
if (parsed && typeof parsed === 'object' && Object.prototype.hasOwnProperty.call(parsed, 'data')) {
|
||||
return parsed.data;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function generateYydsMailLocalPart() {
|
||||
const letters = 'abcdefghijklmnopqrstuvwxyz';
|
||||
const digits = '0123456789';
|
||||
const chars = [];
|
||||
for (let i = 0; i < 6; i += 1) chars.push(letters[Math.floor(Math.random() * letters.length)]);
|
||||
for (let i = 0; i < 4; i += 1) chars.push(digits[Math.floor(Math.random() * digits.length)]);
|
||||
for (let i = chars.length - 1; i > 0; i -= 1) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[chars[i], chars[j]] = [chars[j], chars[i]];
|
||||
}
|
||||
return chars.join('');
|
||||
}
|
||||
|
||||
async function fetchYydsMailAddress(state, options = {}) {
|
||||
throwIfStopped();
|
||||
const latestState = state || await getState();
|
||||
const config = ensureYydsMailConfig(latestState, { requireApiKey: true });
|
||||
const localPart = String(options.localPart || options.name || '').trim().toLowerCase()
|
||||
|| generateYydsMailLocalPart();
|
||||
const data = await requestYydsMailJson(config, '/accounts', {
|
||||
method: 'POST',
|
||||
auth: 'apiKey',
|
||||
payload: { localPart },
|
||||
});
|
||||
const inbox = normalizeYydsMailInbox(data);
|
||||
if (!inbox.address || !inbox.token) {
|
||||
throw new Error('YYDS Mail 创建邮箱成功,但未返回可用 address/token。');
|
||||
}
|
||||
|
||||
await setState({ currentYydsMailInbox: inbox });
|
||||
await persistResolvedEmailState(latestState, inbox.address, {
|
||||
source: `generated:${YYDS_MAIL_PROVIDER}`,
|
||||
preserveAccountIdentity: Boolean(options?.preserveAccountIdentity),
|
||||
});
|
||||
await addLog(`YYDS Mail:已创建邮箱 ${inbox.address}`, 'ok');
|
||||
return inbox.address;
|
||||
}
|
||||
|
||||
function resolveYydsMailInbox(state = {}) {
|
||||
const config = getYydsMailConfig(state);
|
||||
if (config.currentInbox?.address && config.currentInbox?.token) {
|
||||
return config.currentInbox;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveYydsMailPollTargetEmail(state = {}, pollPayload = {}) {
|
||||
return normalizeYydsMailAddress(pollPayload.targetEmail)
|
||||
|| resolveYydsMailInbox(state)?.address
|
||||
|| normalizeYydsMailAddress(state.email);
|
||||
}
|
||||
|
||||
async function listYydsMailMessages(state, options = {}) {
|
||||
const latestState = state || await getState();
|
||||
const inbox = resolveYydsMailInbox(latestState);
|
||||
const config = {
|
||||
...ensureYydsMailConfig(latestState, { requireInbox: true }),
|
||||
currentInbox: inbox,
|
||||
};
|
||||
const address = normalizeYydsMailAddress(options.address) || inbox.address;
|
||||
const payload = await requestYydsMailJson(config, '/messages', {
|
||||
method: 'GET',
|
||||
auth: 'temp',
|
||||
params: {
|
||||
address,
|
||||
limit: Number(options.limit) || 20,
|
||||
},
|
||||
});
|
||||
return {
|
||||
config,
|
||||
messages: normalizeYydsMailMessages(payload),
|
||||
};
|
||||
}
|
||||
|
||||
async function getYydsMailMessageDetail(state, messageId, options = {}) {
|
||||
const latestState = state || await getState();
|
||||
const inbox = resolveYydsMailInbox(latestState);
|
||||
const config = {
|
||||
...ensureYydsMailConfig(latestState, { requireInbox: true }),
|
||||
currentInbox: inbox,
|
||||
};
|
||||
const address = normalizeYydsMailAddress(options.address) || inbox.address;
|
||||
const payload = await requestYydsMailJson(config, `/messages/${encodeURIComponent(messageId)}`, {
|
||||
method: 'GET',
|
||||
auth: 'temp',
|
||||
params: { address },
|
||||
});
|
||||
return normalizeYydsMailMessageDetail(payload);
|
||||
}
|
||||
|
||||
function summarizeYydsMailMessagesForLog(messages) {
|
||||
return (messages || [])
|
||||
.slice()
|
||||
.sort((left, right) => {
|
||||
const leftTime = Date.parse(left.receivedDateTime || '') || 0;
|
||||
const rightTime = Date.parse(right.receivedDateTime || '') || 0;
|
||||
return rightTime - leftTime;
|
||||
})
|
||||
.slice(0, 3)
|
||||
.map((message) => {
|
||||
const receivedAt = message?.receivedDateTime || '未知时间';
|
||||
const sender = message?.from?.emailAddress?.address || '未知发件人';
|
||||
const subject = message?.subject || '(无主题)';
|
||||
const preview = String(message?.bodyPreview || '').replace(/\s+/g, ' ').trim().slice(0, 80);
|
||||
return `${receivedAt} | ${sender} | ${subject} | ${preview}`;
|
||||
})
|
||||
.join(' || ');
|
||||
}
|
||||
|
||||
async function hydrateYydsMailMessageDetails(state, messages, address) {
|
||||
const details = [];
|
||||
for (const message of (messages || []).slice(0, 8)) {
|
||||
throwIfStopped();
|
||||
if (!message?.id) {
|
||||
details.push(message);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
details.push(await getYydsMailMessageDetail(state, message.id, { address }));
|
||||
} catch (err) {
|
||||
await addLog(`YYDS Mail:读取邮件详情 ${message.id} 失败:${err.message}`, 'warn');
|
||||
details.push(message);
|
||||
}
|
||||
}
|
||||
return details.filter(Boolean);
|
||||
}
|
||||
|
||||
async function pollYydsMailVerificationCode(step, state, pollPayload = {}) {
|
||||
const latestState = state || await getState();
|
||||
const targetEmail = resolveYydsMailPollTargetEmail(latestState, pollPayload);
|
||||
if (!targetEmail) {
|
||||
throw new Error('YYDS Mail 轮询前缺少目标邮箱地址,请先获取邮箱。');
|
||||
}
|
||||
|
||||
await addLog(`步骤 ${step}:正在轮询 YYDS Mail 邮件(${targetEmail})...`, 'info');
|
||||
const maxAttempts = Number(pollPayload.maxAttempts) || 5;
|
||||
const intervalMs = Number(pollPayload.intervalMs) || 3000;
|
||||
let lastError = null;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
||||
throwIfStopped();
|
||||
try {
|
||||
const { messages } = await listYydsMailMessages(latestState, {
|
||||
address: targetEmail,
|
||||
limit: pollPayload.limit || 20,
|
||||
});
|
||||
const detailedMessages = await hydrateYydsMailMessageDetails(latestState, messages, targetEmail);
|
||||
const matchResult = pickVerificationMessageWithTimeFallback(detailedMessages, {
|
||||
afterTimestamp: pollPayload.filterAfterTimestamp || 0,
|
||||
senderFilters: pollPayload.senderFilters || [],
|
||||
subjectFilters: pollPayload.subjectFilters || [],
|
||||
requiredKeywords: pollPayload.requiredKeywords || [],
|
||||
codePatterns: pollPayload.codePatterns || [],
|
||||
excludeCodes: pollPayload.excludeCodes || [],
|
||||
});
|
||||
const match = matchResult.match;
|
||||
if (match?.code) {
|
||||
if (matchResult.usedRelaxedFilters) {
|
||||
const fallbackLabel = matchResult.usedTimeFallback ? '宽松匹配 + 时间回退' : '宽松匹配';
|
||||
await addLog(`步骤 ${step}:严格规则未命中,已改用 ${fallbackLabel} 并命中 YYDS Mail 验证码。`, 'warn');
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
code: match.code,
|
||||
emailTimestamp: match.receivedAt || Date.now(),
|
||||
mailId: match.message?.id || '',
|
||||
};
|
||||
}
|
||||
|
||||
lastError = new Error(`步骤 ${step}:暂未在 YYDS Mail 中找到匹配验证码(${attempt}/${maxAttempts})。`);
|
||||
await addLog(lastError.message, attempt === maxAttempts ? 'warn' : 'info');
|
||||
const sample = summarizeYydsMailMessagesForLog(detailedMessages.length ? detailedMessages : messages);
|
||||
if (sample) {
|
||||
await addLog(`步骤 ${step}:最近邮件样本:${sample}`, 'info');
|
||||
}
|
||||
} catch (err) {
|
||||
lastError = err;
|
||||
await addLog(`步骤 ${step}:YYDS Mail 轮询失败:${err.message}`, 'warn');
|
||||
}
|
||||
if (attempt < maxAttempts) {
|
||||
await sleepWithStop(intervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError || new Error(`步骤 ${step}:未在 YYDS Mail 中找到新的匹配验证码。`);
|
||||
}
|
||||
|
||||
async function clearYydsMailRuntimeState(options = {}) {
|
||||
await setState({
|
||||
currentYydsMailInbox: null,
|
||||
...(options.clearEmail ? { email: null } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
clearYydsMailRuntimeState,
|
||||
ensureYydsMailConfig,
|
||||
fetchYydsMailAddress,
|
||||
getYydsMailConfig,
|
||||
getYydsMailMessageDetail,
|
||||
listYydsMailMessages,
|
||||
pollYydsMailVerificationCode,
|
||||
requestYydsMailJson,
|
||||
resolveYydsMailPollTargetEmail,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createYydsMailProvider,
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user