feat: 新增 YYDS Mail 邮箱服务端点

This commit is contained in:
Q3CC
2026-05-16 21:58:14 +08:00
parent cfd2ac14e5
commit b2c8280476
14 changed files with 1211 additions and 3 deletions
+14
View File
@@ -688,12 +688,15 @@ test('resetState preserves LuckMail session config, used map, and preserve tag c
" luckmailBaseUrl: 'https://mails.luckyous.com',",
" luckmailEmailType: 'ms_graph',",
" luckmailDomain: '',",
" yydsMailApiKey: '',",
" yydsMailBaseUrl: 'https://maliapi.215.im/v1',",
" panelMode: 'cpa',",
' luckmailUsedPurchases: {},',
' luckmailPreserveTagId: 0,',
" luckmailPreserveTagName: '保留',",
" currentLuckmailPurchase: { token: 'stale' },",
" currentLuckmailMailCursor: { messageId: 'stale' },",
" currentYydsMailInbox: { address: 'stale@example.com', token: 'stale-token' },",
' currentPhoneActivation: null,',
' reusablePhoneActivation: null,',
' email: null,',
@@ -725,6 +728,12 @@ test('resetState preserves LuckMail session config, used map, and preserve tag c
'function normalizeLuckmailUsedPurchases(value) {',
' return value || {};',
'}',
'function normalizeYydsMailApiKey(value) {',
" return String(value || '').trim();",
'}',
'function normalizeYydsMailBaseUrl(value) {',
" return (String(value || '').trim() || 'https://maliapi.215.im/v1').replace(/\\/$/, '');",
'}',
'async function getPersistedSettings() {',
" return { mailProvider: '163' };",
'}',
@@ -752,6 +761,8 @@ test('resetState preserves LuckMail session config, used map, and preserve tag c
" luckmailUsedPurchases: { 88: true },",
' luckmailPreserveTagId: 9,',
" luckmailPreserveTagName: '保留',",
" yydsMailApiKey: 'AC-session',",
" yydsMailBaseUrl: 'https://maliapi.215.im/v1/',",
' };',
' },',
' async clear() {',
@@ -786,6 +797,9 @@ test('resetState preserves LuckMail session config, used map, and preserve tag c
assert.equal(snapshot.storedPayload.luckmailPreserveTagName, '保留');
assert.equal(snapshot.storedPayload.currentLuckmailPurchase, null);
assert.equal(snapshot.storedPayload.currentLuckmailMailCursor, null);
assert.equal(snapshot.storedPayload.yydsMailApiKey, 'AC-session');
assert.equal(snapshot.storedPayload.yydsMailBaseUrl, 'https://maliapi.215.im/v1');
assert.equal(snapshot.storedPayload.currentYydsMailInbox, null);
assert.deepStrictEqual(snapshot.storedPayload.reusablePhoneActivation, {
activationId: 'rx-001',
phoneNumber: '66951112222',
@@ -15,3 +15,38 @@ test('verification flow module exposes a factory', () => {
assert.equal(typeof api?.createVerificationFlowHelpers, 'function');
});
test('verification flow routes YYDS Mail provider to background poller', async () => {
const source = fs.readFileSync('background/verification-flow.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundVerificationFlow;`)(globalScope);
const pollCalls = [];
const helpers = api.createVerificationFlowHelpers({
addLog: async () => {},
buildVerificationPollPayload: () => ({ maxAttempts: 1, intervalMs: 1 }),
getState: async () => ({}),
getTabId: async () => 1,
isStopError: () => false,
pollYydsMailVerificationCode: async (step, state, payload) => {
pollCalls.push({ step, state, payload });
return { ok: true, code: '123456', emailTimestamp: 1, mailId: 'msg-1' };
},
sendToContentScript: async () => ({}),
setState: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
YYDS_MAIL_PROVIDER: 'yyds-mail',
});
const result = await helpers.pollFreshVerificationCode(
4,
{ mailProvider: 'yyds-mail' },
{ provider: 'yyds-mail', label: 'YYDS Mail' },
{ disableTimeBudgetCap: true }
);
assert.equal(result.code, '123456');
assert.equal(pollCalls.length, 1);
assert.equal(pollCalls[0].step, 4);
assert.equal(pollCalls[0].payload.maxAttempts, 1);
});
+12
View File
@@ -3,6 +3,7 @@ const assert = require('node:assert/strict');
const {
HOTMAIL_PROVIDER,
YYDS_MAIL_PROVIDER,
getIcloudForwardMailConfig,
getIcloudForwardMailProviderOptions,
getMailProviderConfig,
@@ -14,6 +15,7 @@ const {
test('normalizeMailProvider accepts 126 and falls back to 163', () => {
assert.equal(normalizeMailProvider('126'), '126');
assert.equal(normalizeMailProvider('163-vip'), '163-vip');
assert.equal(normalizeMailProvider(YYDS_MAIL_PROVIDER), YYDS_MAIL_PROVIDER);
assert.equal(normalizeMailProvider('unknown-provider'), '163');
});
@@ -38,6 +40,16 @@ test('getMailProviderConfig preserves the hotmail provider sentinel', () => {
);
});
test('getMailProviderConfig preserves the YYDS Mail provider sentinel', () => {
assert.deepEqual(
getMailProviderConfig({ mailProvider: YYDS_MAIL_PROVIDER }),
{
provider: YYDS_MAIL_PROVIDER,
label: 'YYDS Mail',
}
);
});
test('iCloud forward mailbox helpers normalize and expose supported providers', () => {
assert.equal(normalizeIcloudTargetMailboxType('forward-mailbox'), 'forward-mailbox');
assert.equal(normalizeIcloudTargetMailboxType('unknown'), 'icloud-inbox');
+185
View File
@@ -0,0 +1,185 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const utils = require('../yyds-mail-utils.js');
require('../background/yyds-mail-provider.js');
function createProviderApi(options = {}) {
const {
state = {
yydsMailApiKey: 'AC-demo',
yydsMailBaseUrl: 'https://maliapi.215.im/v1',
currentYydsMailInbox: null,
email: '',
},
fetchImpl,
} = options;
let currentState = { ...state };
const logs = [];
const persistCalls = [];
const stateUpdates = [];
const calls = [];
const api = globalThis.MultiPageBackgroundYydsMailProvider.createYydsMailProvider({
addLog: async (message, level) => logs.push({ message, level }),
buildYydsMailHeaders: utils.buildYydsMailHeaders,
DEFAULT_YYDS_MAIL_BASE_URL: utils.DEFAULT_YYDS_MAIL_BASE_URL,
fetchImpl: fetchImpl || (async (url, request = {}) => {
calls.push({
url: String(url),
method: request.method,
headers: request.headers,
body: request.body ? JSON.parse(request.body) : undefined,
});
if (String(url).endsWith('/accounts')) {
return {
ok: true,
text: async () => JSON.stringify({
success: true,
data: {
id: 'inbox-1',
address: 'fresh@example.com',
token: 'temp-token',
expiresAt: '2026-03-15T12:00:00Z',
},
}),
};
}
if (String(url).includes('/messages/msg-1')) {
return {
ok: true,
text: async () => JSON.stringify({
success: true,
data: {
id: 'msg-1',
from: { address: 'noreply@tm.openai.com' },
subject: 'OpenAI verification code',
text: 'Your ChatGPT code is 987654.',
createdAt: '2026-03-14T12:30:00Z',
},
}),
};
}
if (String(url).includes('/messages')) {
return {
ok: true,
text: async () => JSON.stringify({
success: true,
data: {
messages: [{
id: 'msg-1',
from: { address: 'noreply@tm.openai.com' },
subject: 'OpenAI verification code',
createdAt: '2026-03-14T12:30:00Z',
}],
},
}),
};
}
throw new Error(`unexpected URL ${url}`);
}),
getState: async () => currentState,
joinYydsMailUrl: utils.joinYydsMailUrl,
normalizeYydsMailAddress: utils.normalizeYydsMailAddress,
normalizeYydsMailApiKey: utils.normalizeYydsMailApiKey,
normalizeYydsMailBaseUrl: utils.normalizeYydsMailBaseUrl,
normalizeYydsMailCurrentInbox: utils.normalizeYydsMailCurrentInbox,
normalizeYydsMailInbox: utils.normalizeYydsMailInbox,
normalizeYydsMailMessageDetail: utils.normalizeYydsMailMessageDetail,
normalizeYydsMailMessages: utils.normalizeYydsMailMessages,
persistRegistrationEmailState: async (callState, email, persistOptions) => {
persistCalls.push({ state: callState, email, options: persistOptions });
currentState = { ...currentState, email };
},
pickVerificationMessageWithTimeFallback: (messages) => {
const match = messages.find((message) => message.verification_code);
return match
? {
match: {
code: match.verification_code,
receivedAt: Date.parse(match.receivedDateTime),
message: match,
},
usedRelaxedFilters: false,
usedTimeFallback: false,
}
: { match: null, usedRelaxedFilters: false, usedTimeFallback: false };
},
setState: async (updates) => {
stateUpdates.push(updates);
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
YYDS_MAIL_PROVIDER: utils.YYDS_MAIL_PROVIDER,
});
return {
...api,
snapshot() {
return { calls, currentState, logs, persistCalls, stateUpdates };
},
};
}
test('fetchYydsMailAddress creates an inbox with localPart only and stores the returned address/token', async () => {
const api = createProviderApi();
const email = await api.fetchYydsMailAddress(null, { localPart: 'fresh' });
const snapshot = api.snapshot();
assert.equal(email, 'fresh@example.com');
assert.equal(snapshot.calls[0].url, 'https://maliapi.215.im/v1/accounts');
assert.equal(snapshot.calls[0].method, 'POST');
assert.deepEqual(snapshot.calls[0].body, { localPart: 'fresh' });
assert.equal(snapshot.calls[0].headers['X-API-Key'], 'AC-demo');
assert.equal(snapshot.calls[0].headers.Authorization, undefined);
assert.equal(snapshot.currentState.currentYydsMailInbox.address, 'fresh@example.com');
assert.equal(snapshot.currentState.currentYydsMailInbox.token, 'temp-token');
assert.equal(snapshot.persistCalls[0].email, 'fresh@example.com');
});
test('pollYydsMailVerificationCode lists messages with temp token and reads detail before matching code', async () => {
const api = createProviderApi({
state: {
yydsMailApiKey: 'AC-demo',
yydsMailBaseUrl: 'https://maliapi.215.im/v1',
currentYydsMailInbox: {
id: 'inbox-1',
address: 'fresh@example.com',
token: 'temp-token',
},
email: 'fresh@example.com',
},
});
const result = await api.pollYydsMailVerificationCode(4, null, {
maxAttempts: 1,
intervalMs: 1,
senderFilters: ['openai'],
subjectFilters: ['code'],
});
const snapshot = api.snapshot();
const listCall = snapshot.calls.find((call) => call.url.includes('/messages?'));
const detailCall = snapshot.calls.find((call) => call.url.includes('/messages/msg-1'));
assert.equal(result.code, '987654');
assert.equal(listCall.headers.Authorization, 'Bearer temp-token');
assert.equal(listCall.headers['X-API-Key'], undefined);
assert.match(listCall.url, /address=fresh%40example\.com/);
assert.equal(detailCall.headers.Authorization, 'Bearer temp-token');
});
test('clearYydsMailRuntimeState clears current inbox and optionally current email', async () => {
const api = createProviderApi({
state: {
currentYydsMailInbox: { address: 'fresh@example.com', token: 'temp-token' },
email: 'fresh@example.com',
},
});
await api.clearYydsMailRuntimeState({ clearEmail: true });
assert.deepEqual(api.snapshot().stateUpdates.at(-1), {
currentYydsMailInbox: null,
email: null,
});
});
+91
View File
@@ -0,0 +1,91 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const {
DEFAULT_YYDS_MAIL_BASE_URL,
buildYydsMailHeaders,
joinYydsMailUrl,
normalizeYydsMailBaseUrl,
normalizeYydsMailInbox,
normalizeYydsMailMessageDetail,
normalizeYydsMailMessages,
} = require('../yyds-mail-utils.js');
test('normalizeYydsMailBaseUrl defaults to official v1 endpoint and trims trailing slash', () => {
assert.equal(normalizeYydsMailBaseUrl(''), DEFAULT_YYDS_MAIL_BASE_URL);
assert.equal(normalizeYydsMailBaseUrl('maliapi.215.im/v1/'), DEFAULT_YYDS_MAIL_BASE_URL);
assert.equal(normalizeYydsMailBaseUrl('http://127.0.0.1:8787/v1/'), 'http://127.0.0.1:8787/v1');
});
test('buildYydsMailHeaders separates API key creation auth from temp-token read auth', () => {
assert.deepEqual(buildYydsMailHeaders({ apiKey: 'AC-demo' }, { json: true }), {
'X-API-Key': 'AC-demo',
'Content-Type': 'application/json',
Accept: 'application/json',
});
assert.deepEqual(
buildYydsMailHeaders(
{ apiKey: 'AC-demo', token: 'temp-token' },
{ tempToken: 'temp-token', includeConfigApiKey: false }
),
{
Authorization: 'Bearer temp-token',
Accept: 'application/json',
}
);
});
test('joinYydsMailUrl appends query parameters to the normalized base URL', () => {
assert.equal(
joinYydsMailUrl('https://maliapi.215.im/v1/', '/messages', { address: 'a+b@example.com', limit: 20 }),
'https://maliapi.215.im/v1/messages?address=a%2Bb%40example.com&limit=20'
);
});
test('normalizeYydsMailInbox keeps the final returned address and temp token', () => {
assert.deepEqual(normalizeYydsMailInbox({
id: 'inbox-1',
address: 'User@Example.com',
token: 'jwt-token',
expiresAt: '2026-03-15T12:00:00Z',
}), {
id: 'inbox-1',
address: 'user@example.com',
token: 'jwt-token',
expiresAt: '2026-03-15T12:00:00Z',
isActive: true,
createdAt: null,
raw: {
id: 'inbox-1',
address: 'User@Example.com',
token: 'jwt-token',
expiresAt: '2026-03-15T12:00:00Z',
},
});
});
test('normalizeYydsMailMessages and detail expose Graph-like fields for verification matching', () => {
const messages = normalizeYydsMailMessages({
messages: [{
id: 'msg-1',
from: { address: 'noreply@tm.openai.com' },
to: [{ address: 'User@Example.com' }],
subject: 'OpenAI verification code',
createdAt: '2026-03-14T12:30:00Z',
}],
});
assert.equal(messages[0].id, 'msg-1');
assert.equal(messages[0].address, 'user@example.com');
assert.equal(messages[0].from.emailAddress.address, 'noreply@tm.openai.com');
const detail = normalizeYydsMailMessageDetail({
id: 'msg-1',
subject: 'OpenAI verification code',
from: { address: 'noreply@tm.openai.com' },
text: 'Your ChatGPT code is 654321.',
createdAt: '2026-03-14T12:30:00Z',
});
assert.equal(detail.verification_code, '654321');
assert.match(detail.bodyPreview, /654321/);
});