feat: add hotmail mail api account pool flow

This commit is contained in:
baiyuechu
2026-04-11 10:37:43 +08:00
parent ba128ed8d6
commit 8a9d69d325
16 changed files with 3059 additions and 89 deletions
+83
View File
@@ -0,0 +1,83 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const {
getActivationStrategy,
isRecoverableStep9AuthFailure,
} = require('../content/activation-utils.js');
test('getActivationStrategy prefers requestSubmit for submit buttons inside forms', () => {
assert.deepEqual(
getActivationStrategy({
tagName: 'button',
type: 'submit',
hasForm: true,
pathname: '/email-verification',
}),
{ method: 'requestSubmit' }
);
});
test('getActivationStrategy uses native click for non-submit actions', () => {
assert.deepEqual(
getActivationStrategy({
tagName: 'button',
type: 'button',
hasForm: true,
}),
{ method: 'click' }
);
assert.deepEqual(
getActivationStrategy({
tagName: 'a',
type: '',
hasForm: false,
}),
{ method: 'click' }
);
});
test('getActivationStrategy only uses requestSubmit on email verification routes', () => {
assert.deepEqual(
getActivationStrategy({
tagName: 'button',
type: 'submit',
hasForm: true,
pathname: '/u/signup/details',
}),
{ method: 'click' }
);
assert.deepEqual(
getActivationStrategy({
tagName: 'button',
type: 'submit',
hasForm: true,
pathname: '/email-verification',
}),
{ method: 'requestSubmit' }
);
});
test('isRecoverableStep9AuthFailure matches timeout and CPA auth failure statuses', () => {
assert.equal(
isRecoverableStep9AuthFailure('认证失败: Timeout waiting for OAuth callback'),
true
);
assert.equal(
isRecoverableStep9AuthFailure('认证失败: Request failed with status code 502'),
true
);
assert.equal(
isRecoverableStep9AuthFailure('认证成功!'),
false
);
assert.equal(
isRecoverableStep9AuthFailure('等待中'),
false
);
});
+468
View File
@@ -0,0 +1,468 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const {
buildHotmailMailApiLatestUrl,
extractVerificationCodeFromMessage,
filterHotmailAccountsByUsage,
extractVerificationCode,
getLatestHotmailMessage,
getHotmailBulkActionLabel,
getHotmailListToggleLabel,
getHotmailMailApiRequestConfig,
getHotmailVerificationPollConfig,
getHotmailVerificationRequestTimestamp,
normalizeHotmailMailApiMessages,
parseHotmailImportText,
pickHotmailAccountForRun,
pickVerificationMessage,
pickVerificationMessageWithFallback,
pickVerificationMessageWithTimeFallback,
shouldClearHotmailCurrentSelection,
upsertHotmailAccountInList,
} = require('../hotmail-utils.js');
test('pickHotmailAccountForRun prefers authorized account with oldest lastUsedAt', () => {
const now = Date.UTC(2026, 3, 10, 10, 0, 0);
const accounts = [
{
id: 'recent',
email: 'recent@hotmail.com',
status: 'authorized',
refreshToken: 'rt-recent',
lastUsedAt: now - 1_000,
},
{
id: 'oldest',
email: 'oldest@hotmail.com',
status: 'authorized',
refreshToken: 'rt-oldest',
lastUsedAt: now - 50_000,
},
{
id: 'pending',
email: 'pending@hotmail.com',
status: 'pending',
refreshToken: '',
lastUsedAt: 0,
},
];
const selected = pickHotmailAccountForRun(accounts, { now });
assert.equal(selected.id, 'oldest');
});
test('pickHotmailAccountForRun skips used accounts but ignores legacy enabled flag', () => {
const accounts = [
{
id: 'disabled',
email: 'disabled@hotmail.com',
status: 'authorized',
refreshToken: 'rt-disabled',
enabled: false,
used: false,
lastUsedAt: 1,
},
{
id: 'used',
email: 'used@hotmail.com',
status: 'authorized',
refreshToken: 'rt-used',
enabled: true,
used: true,
lastUsedAt: 0,
},
{
id: 'available',
email: 'available@hotmail.com',
status: 'authorized',
refreshToken: 'rt-available',
enabled: true,
used: false,
lastUsedAt: 2,
},
];
const selected = pickHotmailAccountForRun(accounts, {});
assert.equal(selected.id, 'disabled');
});
test('pickHotmailAccountForRun returns null for used-only pools', () => {
assert.equal(
pickHotmailAccountForRun([{
id: 'used-only',
email: 'used-only@hotmail.com',
status: 'authorized',
refreshToken: 'rt-used-only',
used: true,
lastUsedAt: 0,
}], {}),
null
);
});
test('pickHotmailAccountForRun falls back to never-used authorized account first', () => {
const accounts = [
{
id: 'used',
email: 'used@hotmail.com',
status: 'authorized',
refreshToken: 'rt-used',
lastUsedAt: Date.UTC(2026, 3, 10, 9, 0, 0),
},
{
id: 'fresh',
email: 'fresh@hotmail.com',
status: 'authorized',
refreshToken: 'rt-fresh',
lastUsedAt: 0,
},
];
const selected = pickHotmailAccountForRun(accounts, { now: Date.UTC(2026, 3, 10, 10, 0, 0) });
assert.equal(selected.id, 'fresh');
});
test('upsertHotmailAccountInList replaces matching account state by id', () => {
const accounts = [
{
id: 'active',
email: 'active@hotmail.com',
status: 'authorized',
used: false,
},
{
id: 'other',
email: 'other@hotmail.com',
status: 'authorized',
used: false,
},
];
const nextAccounts = upsertHotmailAccountInList(accounts, {
id: 'active',
email: 'active@hotmail.com',
status: 'authorized',
used: true,
});
assert.deepEqual(nextAccounts, [
{
id: 'active',
email: 'active@hotmail.com',
status: 'authorized',
used: true,
},
{
id: 'other',
email: 'other@hotmail.com',
status: 'authorized',
used: false,
},
]);
});
test('shouldClearHotmailCurrentSelection returns true only when account becomes used', () => {
assert.equal(shouldClearHotmailCurrentSelection({
id: 'used',
used: true,
}), true);
assert.equal(shouldClearHotmailCurrentSelection({
id: 'available',
used: false,
}), false);
});
test('extractVerificationCode returns first six-digit code from multilingual mail text', () => {
assert.equal(extractVerificationCode('你的 ChatGPT 验证码为 370794,请勿泄露。'), '370794');
assert.equal(extractVerificationCode('Your verification code is 654321.'), '654321');
assert.equal(extractVerificationCode('No code here'), null);
});
test('extractVerificationCodeFromMessage reads code from the latest message subject or preview', () => {
assert.equal(
extractVerificationCodeFromMessage({
subject: '你的 ChatGPT 代码为 192742',
bodyPreview: 'OpenAI 验证邮件',
from: { emailAddress: { address: 'noreply@openai.com' } },
}),
'192742'
);
assert.equal(
extractVerificationCodeFromMessage({
subject: 'OpenAI security message',
bodyPreview: 'Your verification code is 654321.',
from: { emailAddress: { address: 'noreply@openai.com' } },
}),
'654321'
);
});
test('getHotmailListToggleLabel reflects expanded state and account count', () => {
assert.equal(getHotmailListToggleLabel(false, 0), '展开列表');
assert.equal(getHotmailListToggleLabel(false, 7), '展开列表(7');
assert.equal(getHotmailListToggleLabel(true, 7), '收起列表(7');
});
test('filterHotmailAccountsByUsage can pick only used accounts or return all accounts', () => {
const accounts = [
{ id: 'used-1', email: 'used-1@hotmail.com', used: true },
{ id: 'fresh-1', email: 'fresh-1@hotmail.com', used: false },
{ id: 'used-2', email: 'used-2@hotmail.com', used: true },
];
assert.deepEqual(
filterHotmailAccountsByUsage(accounts, 'used').map((account) => account.id),
['used-1', 'used-2']
);
assert.deepEqual(
filterHotmailAccountsByUsage(accounts, 'all').map((account) => account.id),
['used-1', 'fresh-1', 'used-2']
);
});
test('getHotmailBulkActionLabel reflects action type and count', () => {
assert.equal(getHotmailBulkActionLabel('used', 0), '清空已用');
assert.equal(getHotmailBulkActionLabel('used', 3), '清空已用(3');
assert.equal(getHotmailBulkActionLabel('all', 5), '全部删除(5');
});
test('getLatestHotmailMessage picks the newest received mail', () => {
const latest = getLatestHotmailMessage([
{
id: 'older',
subject: 'older',
receivedDateTime: '2026-04-11T00:01:00.000Z',
},
{
id: 'newest',
subject: 'newest',
receivedDateTime: '2026-04-11T00:05:00.000Z',
},
{
id: 'middle',
subject: 'middle',
receivedDateTime: '2026-04-11T00:03:00.000Z',
},
]);
assert.equal(latest.id, 'newest');
});
test('pickVerificationMessage filters by time, sender, subject, and excluded codes', () => {
const messages = [
{
id: 'old-mail',
subject: 'Your code is 111111',
from: { emailAddress: { address: 'noreply@openai.com' } },
bodyPreview: '111111',
receivedDateTime: '2026-04-10T09:00:00.000Z',
},
{
id: 'wrong-sender',
subject: 'Your code is 222222',
from: { emailAddress: { address: 'noreply@example.com' } },
bodyPreview: '222222',
receivedDateTime: '2026-04-10T10:01:00.000Z',
},
{
id: 'good-mail',
subject: 'ChatGPT verification code 333333',
from: { emailAddress: { address: 'noreply@openai.com' } },
bodyPreview: 'Use 333333 to continue',
receivedDateTime: '2026-04-10T10:02:00.000Z',
},
{
id: 'excluded-mail',
subject: 'ChatGPT verification code 444444',
from: { emailAddress: { address: 'noreply@openai.com' } },
bodyPreview: 'Use 444444 to continue',
receivedDateTime: '2026-04-10T10:03:00.000Z',
},
];
const match = pickVerificationMessage(messages, {
afterTimestamp: Date.UTC(2026, 3, 10, 10, 0, 0),
senderFilters: ['openai', 'noreply'],
subjectFilters: ['verification', 'code', 'chatgpt'],
excludeCodes: ['444444'],
});
assert.equal(match.message.id, 'good-mail');
assert.equal(match.code, '333333');
});
test('pickVerificationMessageWithFallback returns relaxed match when strict filters miss but recent code exists', () => {
const messages = [
{
id: 'login-mail',
subject: 'Use this security code to continue 555666',
from: { emailAddress: { address: 'account-security@openai.com' } },
bodyPreview: 'Your one-time security code is 555666',
receivedDateTime: '2026-04-10T10:05:00.000Z',
},
];
const result = pickVerificationMessageWithFallback(messages, {
afterTimestamp: Date.UTC(2026, 3, 10, 10, 0, 0),
senderFilters: ['noreply'],
subjectFilters: ['verification'],
excludeCodes: [],
});
assert.equal(result.match.message.id, 'login-mail');
assert.equal(result.match.code, '555666');
assert.equal(result.usedRelaxedFilters, true);
assert.equal(result.usedTimeFallback, false);
});
test('pickVerificationMessageWithTimeFallback can ignore afterTimestamp when latest code mail is just outside flow window', () => {
const messages = [
{
id: 'slightly-old-mail',
subject: '你的 ChatGPT 代码为 141735',
from: { emailAddress: { address: 'unknown' } },
bodyPreview: 'OpenAI logo ...',
receivedDateTime: '2026-04-10T10:00:02.000Z',
},
];
const result = pickVerificationMessageWithTimeFallback(messages, {
afterTimestamp: Date.UTC(2026, 3, 10, 10, 0, 10),
senderFilters: ['openai', 'noreply'],
subjectFilters: ['verify', 'verification', 'code'],
excludeCodes: [],
});
assert.equal(result.match.message.id, 'slightly-old-mail');
assert.equal(result.match.code, '141735');
assert.equal(result.usedRelaxedFilters, true);
assert.equal(result.usedTimeFallback, true);
});
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://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('normalizeHotmailMailApiMessages maps third-party payload fields into verification message shape', () => {
const messages = normalizeHotmailMailApiMessages([
{
id: 'mail-1',
from: 'noreply@openai.com',
subject: 'ChatGPT verification code',
text: 'Use 135790 to continue',
date: '2026-04-10T10:02:00.000Z',
},
{
message_id: 'mail-2',
sender_email: 'alerts@example.com',
title: 'Ignored',
body: 'No code here',
received_at: '2026-04-10T10:03:00.000Z',
},
]);
assert.deepEqual(messages, [
{
id: 'mail-1',
subject: 'ChatGPT verification code',
from: { emailAddress: { address: 'noreply@openai.com' } },
bodyPreview: 'Use 135790 to continue',
receivedDateTime: '2026-04-10T10:02:00.000Z',
},
{
id: 'mail-2',
subject: 'Ignored',
from: { emailAddress: { address: 'alerts@example.com' } },
bodyPreview: 'No code here',
receivedDateTime: '2026-04-10T10:03:00.000Z',
},
]);
});
test('getHotmailVerificationPollConfig gives Hotmail a slower initial wait and longer polling window', () => {
assert.deepEqual(getHotmailVerificationPollConfig(4), {
initialDelayMs: 5000,
maxAttempts: 12,
intervalMs: 5000,
requestFreshCodeFirst: false,
ignorePersistedLastCode: true,
});
assert.deepEqual(getHotmailVerificationPollConfig(7), {
initialDelayMs: 5000,
maxAttempts: 12,
intervalMs: 5000,
requestFreshCodeFirst: false,
ignorePersistedLastCode: true,
});
});
test('getHotmailVerificationRequestTimestamp prefers actual request timestamps with a safety buffer', () => {
const signupRequestedAt = Date.UTC(2026, 3, 10, 12, 0, 30);
const loginRequestedAt = Date.UTC(2026, 3, 10, 12, 5, 45);
assert.equal(
getHotmailVerificationRequestTimestamp(4, {
signupVerificationRequestedAt: signupRequestedAt,
flowStartTime: signupRequestedAt - 60_000,
}),
signupRequestedAt - 15_000
);
assert.equal(
getHotmailVerificationRequestTimestamp(7, {
loginVerificationRequestedAt: loginRequestedAt,
lastEmailTimestamp: loginRequestedAt - 120_000,
flowStartTime: loginRequestedAt - 300_000,
}),
loginRequestedAt - 15_000
);
});
test('getHotmailMailApiRequestConfig defines a finite timeout for mailbox polling', () => {
assert.deepEqual(getHotmailMailApiRequestConfig(), {
timeoutMs: 15000,
});
});
test('parseHotmailImportText parses account lines in email----password----clientId----token format', () => {
const parsed = parseHotmailImportText(`
账号----密码----ID----Token
JohnRodriguez5425@hotmail.com----nb4ta1OK----9e5f94bc-e8a4-4e73-b8be-63364c29d753----refresh-token-1
alice@hotmail.com----pass-2----client-2----refresh-token-2
`.trim());
assert.deepEqual(parsed, [
{
email: 'JohnRodriguez5425@hotmail.com',
password: 'nb4ta1OK',
clientId: '9e5f94bc-e8a4-4e73-b8be-63364c29d753',
refreshToken: 'refresh-token-1',
},
{
email: 'alice@hotmail.com',
password: 'pass-2',
clientId: 'client-2',
refreshToken: 'refresh-token-2',
},
]);
});