fix: align Hotmail API integration with current dev

- merge the latest dev changes into PR #69 before continuing the review flow
- keep Microsoft API mode while preserving mailbox-by-mailbox polling and verification filters
- update the Hotmail side panel copy and helper coverage for the repaired API path
This commit is contained in:
QLHazyCoder
2026-04-15 01:36:40 +08:00
30 changed files with 8560 additions and 854 deletions
@@ -60,11 +60,20 @@ const bundle = [
extractFunction('hasSavedProgress'),
extractFunction('getRunningSteps'),
extractFunction('getAutoRunStatusPayload'),
extractFunction('createAutoRunRoundSummary'),
extractFunction('normalizeAutoRunRoundSummary'),
extractFunction('buildAutoRunRoundSummaries'),
extractFunction('serializeAutoRunRoundSummaries'),
extractFunction('getAutoRunRoundRetryCount'),
extractFunction('formatAutoRunFailureReasons'),
extractFunction('logAutoRunFinalSummary'),
extractFunction('waitBetweenAutoRunRounds'),
extractFunction('autoRunLoop'),
].join('\n');
const api = new Function(`
const STOP_ERROR_MESSAGE = 'Flow stopped.';
const AUTO_RUN_MAX_RETRIES_PER_ROUND = 3;
const DEFAULT_STATE = {
stepStatuses: {
1: 'pending',
@@ -180,6 +189,20 @@ function cancelPendingCommands() {}
function normalizeAutoRunFallbackThreadIntervalMinutes(value) {
return Math.max(0, Math.floor(Number(value) || 0));
}
function buildAutoRunRoundSummaries(totalRuns, rawSummaries = []) {
return Array.from({ length: totalRuns }, (_, index) => ({
round: index + 1,
status: rawSummaries[index]?.status || 'pending',
attempts: rawSummaries[index]?.attempts || 0,
failureReasons: [...(rawSummaries[index]?.failureReasons || [])],
finalFailureReason: rawSummaries[index]?.finalFailureReason || '',
}));
}
function serializeAutoRunRoundSummaries(totalRuns, roundSummaries = []) {
return buildAutoRunRoundSummaries(totalRuns, roundSummaries);
}
async function logAutoRunFinalSummary() {}
async function waitBetweenAutoRunRounds() {}
const chrome = {
runtime: {
+265
View File
@@ -0,0 +1,265 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const source = fs.readFileSync('background.js', 'utf8');
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
.map((marker) => source.indexOf(marker))
.find((index) => index >= 0);
if (start < 0) {
throw new Error(`missing function ${name}`);
}
let parenDepth = 0;
let signatureEnded = false;
let braceStart = -1;
for (let i = start; i < source.length; i += 1) {
const ch = source[i];
if (ch === '(') {
parenDepth += 1;
} else if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (ch === '{' && signatureEnded) {
braceStart = i;
break;
}
}
if (braceStart < 0) {
throw new Error(`missing body for function ${name}`);
}
let depth = 0;
let end = braceStart;
for (; end < source.length; end += 1) {
const ch = source[end];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return source.slice(start, end);
}
const bundle = [
extractFunction('normalizeEmailGenerator'),
extractFunction('getEmailGeneratorLabel'),
extractFunction('normalizePersistentSettingValue'),
extractFunction('finalizeIcloudAliasAfterSuccessfulFlow'),
].join('\n');
function createApi(overrides = {}) {
return new Function('overrides', `
const HOTMAIL_PROVIDER = 'hotmail-api';
const HOTMAIL_SERVICE_MODE_LOCAL = 'local';
const CLOUDFLARE_TEMP_EMAIL_GENERATOR = 'cloudflare-temp-email';
const DEFAULT_LOCAL_CPA_STEP9_MODE = 'submit';
const DEFAULT_HOTMAIL_REMOTE_BASE_URL = '';
const DEFAULT_HOTMAIL_LOCAL_BASE_URL = 'http://127.0.0.1:17373';
const PERSISTED_SETTING_DEFAULTS = {
mailProvider: '163',
autoStepDelaySeconds: null,
};
const calls = {
setUsed: [],
logs: [],
deletes: [],
listCalls: 0,
};
function normalizeIcloudHost(value) {
const normalized = String(value || '').trim().toLowerCase();
return ['icloud.com', 'icloud.com.cn'].includes(normalized) ? normalized : '';
}
function normalizePanelMode(value = '') {
return String(value || '').trim().toLowerCase() === 'sub2api' ? 'sub2api' : 'cpa';
}
function normalizeMailProvider(value = '') {
return String(value || '').trim().toLowerCase() || '163';
}
function normalizeAutoRunFallbackThreadIntervalMinutes(value) {
return Math.max(0, Math.floor(Number(value) || 0));
}
function normalizeAutoRunDelayMinutes(value) {
return Math.max(1, Math.floor(Number(value) || 30));
}
function normalizeAutoStepDelaySeconds(value, fallback = null) {
const numeric = Number(value);
return Number.isFinite(numeric) ? Math.max(0, Math.floor(numeric)) : fallback;
}
function normalizeHotmailServiceMode() {
return HOTMAIL_SERVICE_MODE_LOCAL;
}
function normalizeHotmailRemoteBaseUrl(value = '') {
return String(value || '').trim() || DEFAULT_HOTMAIL_REMOTE_BASE_URL;
}
function normalizeHotmailLocalBaseUrl(value = '') {
return String(value || '').trim() || DEFAULT_HOTMAIL_LOCAL_BASE_URL;
}
function normalizeCloudflareDomain(value = '') {
return String(value || '').trim().toLowerCase();
}
function normalizeCloudflareDomains(values = []) {
return Array.isArray(values) ? values : [];
}
function normalizeHotmailAccounts(values = []) {
return Array.isArray(values) ? values : [];
}
function getManualAliasUsageMap(state) {
return { ...(state?.manualAliasUsage || {}) };
}
function getPreservedAliasMap(state) {
return { ...(state?.preservedAliases || {}) };
}
function isAliasPreserved(state, email) {
return Boolean(getPreservedAliasMap(state)[String(email || '').trim().toLowerCase()]);
}
async function setIcloudAliasUsedState(payload, options = {}) {
calls.setUsed.push({ payload, options });
}
async function addLog(message, level = 'info') {
calls.logs.push({ message, level });
}
async function deleteIcloudAlias(alias) {
calls.deletes.push(alias);
}
async function listIcloudAliases() {
calls.listCalls += 1;
return overrides.listIcloudAliases ? overrides.listIcloudAliases() : [];
}
function findIcloudAliasByEmail(aliases, email) {
return (aliases || []).find((alias) => String(alias.email || '').toLowerCase() === String(email || '').toLowerCase()) || null;
}
function getErrorMessage(error) {
return String(typeof error === 'string' ? error : error?.message || '');
}
${bundle}
return {
calls,
normalizeEmailGenerator,
getEmailGeneratorLabel,
normalizePersistentSettingValue,
finalizeIcloudAliasAfterSuccessfulFlow,
};
`)(overrides);
}
test('normalizeEmailGenerator and label support icloud', () => {
const api = createApi();
assert.equal(api.normalizeEmailGenerator('icloud'), 'icloud');
assert.equal(api.getEmailGeneratorLabel('icloud'), 'iCloud 隐私邮箱');
});
test('normalizePersistentSettingValue handles icloud settings', () => {
const api = createApi();
assert.equal(api.normalizePersistentSettingValue('icloudHostPreference', 'icloud.com'), 'icloud.com');
assert.equal(api.normalizePersistentSettingValue('icloudHostPreference', 'bad-host'), 'auto');
assert.equal(api.normalizePersistentSettingValue('autoDeleteUsedIcloudAlias', 1), true);
});
test('finalizeIcloudAliasAfterSuccessfulFlow marks icloud aliases as used without deleting when auto-delete is off', async () => {
const api = createApi();
const result = await api.finalizeIcloudAliasAfterSuccessfulFlow({
email: 'alias@icloud.com',
emailGenerator: 'icloud',
autoDeleteUsedIcloudAlias: false,
manualAliasUsage: {},
preservedAliases: {},
});
assert.deepEqual(result, { handled: true, deleted: false });
assert.equal(api.calls.setUsed.length, 1);
assert.equal(api.calls.listCalls, 0);
assert.equal(api.calls.deletes.length, 0);
});
test('finalizeIcloudAliasAfterSuccessfulFlow skips deleting preserved aliases', async () => {
const api = createApi();
const result = await api.finalizeIcloudAliasAfterSuccessfulFlow({
email: 'alias@icloud.com',
emailGenerator: 'icloud',
autoDeleteUsedIcloudAlias: true,
manualAliasUsage: {},
preservedAliases: { 'alias@icloud.com': true },
});
assert.deepEqual(result, { handled: true, deleted: false });
assert.equal(api.calls.setUsed.length, 1);
assert.equal(api.calls.listCalls, 0);
assert.equal(api.calls.deletes.length, 0);
});
test('finalizeIcloudAliasAfterSuccessfulFlow skips deleting aliases that are preserved in the latest alias list', async () => {
const api = createApi({
listIcloudAliases() {
return [
{ email: 'alias@icloud.com', anonymousId: 'anon-1', preserved: true },
];
},
});
const result = await api.finalizeIcloudAliasAfterSuccessfulFlow({
email: 'alias@icloud.com',
emailGenerator: 'icloud',
autoDeleteUsedIcloudAlias: true,
manualAliasUsage: {},
preservedAliases: {},
});
assert.deepEqual(result, { handled: true, deleted: false });
assert.equal(api.calls.setUsed.length, 1);
assert.equal(api.calls.listCalls, 1);
assert.equal(api.calls.deletes.length, 0);
});
test('finalizeIcloudAliasAfterSuccessfulFlow deletes alias when auto-delete is enabled and alias exists', async () => {
const api = createApi({
listIcloudAliases() {
return [
{ email: 'alias@icloud.com', anonymousId: 'anon-1', preserved: false },
];
},
});
const result = await api.finalizeIcloudAliasAfterSuccessfulFlow({
email: 'alias@icloud.com',
emailGenerator: 'icloud',
autoDeleteUsedIcloudAlias: true,
manualAliasUsage: {},
preservedAliases: {},
});
assert.deepEqual(result, { handled: true, deleted: true });
assert.equal(api.calls.setUsed.length, 1);
assert.equal(api.calls.listCalls, 1);
assert.deepEqual(api.calls.deletes, [
{ email: 'alias@icloud.com', anonymousId: 'anon-1', preserved: false },
]);
});
test('finalizeIcloudAliasAfterSuccessfulFlow ignores non-icloud flows', async () => {
const api = createApi();
const result = await api.finalizeIcloudAliasAfterSuccessfulFlow({
email: 'plain@example.com',
emailGenerator: 'duck',
autoDeleteUsedIcloudAlias: true,
manualAliasUsage: {},
preservedAliases: {},
});
assert.deepEqual(result, { handled: false, deleted: false });
assert.equal(api.calls.setUsed.length, 0);
});
+572
View File
@@ -0,0 +1,572 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('background.js', 'utf8');
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
.map((marker) => source.indexOf(marker))
.find((index) => index >= 0);
if (start < 0) {
throw new Error(`missing function ${name}`);
}
let parenDepth = 0;
let signatureEnded = false;
let braceStart = -1;
for (let i = start; i < source.length; i += 1) {
const ch = source[i];
if (ch === '(') {
parenDepth += 1;
} else if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (ch === '{' && signatureEnded) {
braceStart = i;
break;
}
}
if (braceStart < 0) {
throw new Error(`missing body for function ${name}`);
}
let depth = 0;
let end = braceStart;
for (; end < source.length; end += 1) {
const ch = source[end];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return source.slice(start, end);
}
test('ensureLuckmailPurchaseForFlow buys openai mailbox and defaults email type to ms_graph', async () => {
const bundle = [
extractFunction('getLuckmailSessionConfig'),
extractFunction('getCurrentLuckmailPurchase'),
extractFunction('ensureLuckmailPurchaseForFlow'),
].join('\n');
const factory = new Function('initialState', `
let currentState = { ...initialState };
const DEFAULT_LUCKMAIL_PROJECT_CODE = 'openai';
const purchaseCalls = [];
const activateCalls = [];
function normalizeLuckmailBaseUrl(value) {
return String(value || '').trim() || 'https://mails.luckyous.com';
}
function normalizeLuckmailEmailType(value) {
return ['self_built', 'ms_imap', 'ms_graph', 'google_variant'].includes(String(value || '').trim())
? String(value || '').trim()
: 'ms_graph';
}
function normalizeLuckmailPurchase(value) {
return value;
}
function normalizeLuckmailPurchases(value) {
return value.purchases || [];
}
async function getState() {
return currentState;
}
function createLuckmailClient() {
return {
user: {
async purchaseEmails(projectCode, quantity, options) {
purchaseCalls.push({ projectCode, quantity, options });
return {
purchases: [{ id: 15, email_address: 'demo@outlook.com', token: 'tok-1' }],
};
},
},
};
}
async function findReusableLuckmailPurchaseForFlow() {
return null;
}
async function activateLuckmailPurchaseForFlow(state, client, purchase, options) {
activateCalls.push({ state, purchase, options });
currentState.currentLuckmailPurchase = purchase;
currentState.email = purchase.email_address;
return purchase;
}
${bundle}
return {
ensureLuckmailPurchaseForFlow,
snapshot() {
return { currentState, purchaseCalls, activateCalls };
},
};
`);
const api = factory({
luckmailApiKey: 'sk-test',
luckmailBaseUrl: '',
luckmailEmailType: '',
luckmailDomain: '',
currentLuckmailPurchase: null,
email: null,
});
const purchase = await api.ensureLuckmailPurchaseForFlow();
const snapshot = api.snapshot();
assert.equal(purchase.email_address, 'demo@outlook.com');
assert.deepStrictEqual(snapshot.purchaseCalls, [{
projectCode: 'openai',
quantity: 1,
options: {
emailType: 'ms_graph',
domain: undefined,
},
}]);
assert.equal(snapshot.activateCalls[0].options.initializeCursor, false);
assert.equal(snapshot.currentState.email, 'demo@outlook.com');
});
test('ensureLuckmailPurchaseForFlow reuses reusable openai mailbox before buying a new one', async () => {
const bundle = [
extractFunction('getLuckmailSessionConfig'),
extractFunction('getCurrentLuckmailPurchase'),
extractFunction('ensureLuckmailPurchaseForFlow'),
].join('\n');
const factory = new Function('initialState', `
let currentState = { ...initialState };
const DEFAULT_LUCKMAIL_PROJECT_CODE = 'openai';
const purchaseCalls = [];
const activateCalls = [];
function normalizeLuckmailBaseUrl(value) {
return String(value || '').trim() || 'https://mails.luckyous.com';
}
function normalizeLuckmailEmailType(value) {
return ['self_built', 'ms_imap', 'ms_graph', 'google_variant'].includes(String(value || '').trim())
? String(value || '').trim()
: 'ms_graph';
}
function normalizeLuckmailPurchase(value) {
return value;
}
function normalizeLuckmailPurchases(value) {
return value.purchases || [];
}
async function getState() {
return currentState;
}
function createLuckmailClient() {
return {
user: {
async purchaseEmails(projectCode, quantity, options) {
purchaseCalls.push({ projectCode, quantity, options });
return { purchases: [] };
},
},
};
}
async function findReusableLuckmailPurchaseForFlow() {
return {
id: 99,
email_address: 'reuse@outlook.com',
token: 'tok-reuse',
};
}
async function activateLuckmailPurchaseForFlow(state, client, purchase, options) {
activateCalls.push({ state, purchase, options });
currentState.currentLuckmailPurchase = purchase;
currentState.email = purchase.email_address;
return purchase;
}
${bundle}
return {
ensureLuckmailPurchaseForFlow,
snapshot() {
return { currentState, purchaseCalls, activateCalls };
},
};
`);
const api = factory({
luckmailApiKey: 'sk-test',
luckmailBaseUrl: 'https://mails.luckyous.com',
luckmailEmailType: 'ms_imap',
luckmailDomain: 'outlook.com',
currentLuckmailPurchase: null,
email: null,
});
const purchase = await api.ensureLuckmailPurchaseForFlow();
const snapshot = api.snapshot();
assert.equal(purchase.id, 99);
assert.deepStrictEqual(snapshot.purchaseCalls, []);
assert.equal(snapshot.activateCalls[0].options.initializeCursor, true);
assert.match(snapshot.activateCalls[0].options.logMessage, /已复用 openai 邮箱/);
});
test('activateLuckmailPurchaseForFlow builds baseline cursor from existing mails when reusing mailbox', async () => {
const bundle = extractFunction('activateLuckmailPurchaseForFlow');
const factory = new Function(`
let currentPurchase = null;
let currentCursor = null;
let currentEmail = null;
const buildCalls = [];
function normalizeLuckmailPurchase(value) {
return value;
}
async function setLuckmailPurchaseState(value) {
currentPurchase = value;
}
async function setLuckmailMailCursorState(value) {
currentCursor = value;
}
async function setEmailState(value) {
currentEmail = value;
}
async function addLog() {}
function buildLuckmailBaselineCursor(mails) {
buildCalls.push(mails);
return { messageId: 'mail-new', receivedAt: '2026-04-14 13:32:05' };
}
${bundle}
return {
activateLuckmailPurchaseForFlow,
snapshot() {
return { currentPurchase, currentCursor, currentEmail, buildCalls };
},
};
`);
const api = factory();
const client = {
user: {
async getTokenMails() {
return {
mails: [
{ message_id: 'mail-old', received_at: '2026-04-14 13:31:15' },
{ message_id: 'mail-new', received_at: '2026-04-14 13:32:05' },
],
};
},
},
};
await api.activateLuckmailPurchaseForFlow({}, client, {
id: 5,
email_address: 'reuse@outlook.com',
token: 'tok-reuse',
}, {
initializeCursor: true,
logMessage: 'reuse',
});
const snapshot = api.snapshot();
assert.equal(snapshot.currentPurchase.id, 5);
assert.deepStrictEqual(snapshot.currentCursor, {
messageId: 'mail-new',
receivedAt: '2026-04-14 13:32:05',
});
assert.equal(snapshot.currentEmail, 'reuse@outlook.com');
assert.equal(snapshot.buildCalls.length, 1);
});
test('listLuckmailPurchasesByProject only keeps openai purchases', async () => {
const bundle = extractFunction('listLuckmailPurchasesByProject');
const factory = new Function(`
const DEFAULT_LUCKMAIL_PROJECT_CODE = 'openai';
function normalizeLuckmailProjectName(value) {
return String(value || '').trim().toLowerCase();
}
function isLuckmailPurchaseForProject(purchase, projectCode) {
return normalizeLuckmailProjectName(purchase.project_name || purchase.project) === normalizeLuckmailProjectName(projectCode);
}
async function getAllLuckmailPurchases() {
return [
{ id: 1, project_name: 'OpenAi' },
{ id: 2, project_name: 'other' },
{ id: 3, project: 'openai' },
];
}
${bundle}
return { listLuckmailPurchasesByProject };
`);
const api = factory();
const result = await api.listLuckmailPurchasesByProject({}, { projectCode: 'openai' });
assert.deepStrictEqual(result.map((item) => item.id), [1, 3]);
});
test('disableUsedLuckmailPurchases only disables locally used and non-preserved openai mailboxes', async () => {
const bundle = extractFunction('disableUsedLuckmailPurchases');
const factory = new Function(`
let clearedOptions = null;
const disabledCalls = [];
const DEFAULT_LUCKMAIL_PROJECT_CODE = 'openai';
function normalizeLuckmailPurchaseId(value) {
const numeric = Number(value);
return Number.isFinite(numeric) && numeric > 0 ? String(Math.floor(numeric)) : '';
}
async function ensureManualInteractionAllowed() {
return {
luckmailUsedPurchases: { 1: true, 2: true, 3: true },
luckmailPreserveTagId: 9,
luckmailPreserveTagName: '保留',
mailProvider: 'luckmail-api',
};
}
function getLuckmailUsedPurchases(state) {
return state.luckmailUsedPurchases;
}
function getLuckmailPreserveTagInfo(state) {
return {
id: state.luckmailPreserveTagId,
name: state.luckmailPreserveTagName,
};
}
function isLuckmailPurchasePreserved(purchase, options) {
return purchase.tag_id === options.preserveTagId || purchase.tag_name === options.preserveTagName;
}
function createLuckmailClient() {
return {
user: {
async batchSetPurchaseDisabled(ids, disabled) {
disabledCalls.push({ ids, disabled });
},
},
};
}
async function listLuckmailPurchasesByProject() {
return [
{ id: 1, email_address: 'used-1@outlook.com', user_disabled: 0, tag_id: 0, tag_name: '' },
{ id: 2, email_address: 'preserved@outlook.com', user_disabled: 0, tag_id: 9, tag_name: '保留' },
{ id: 3, email_address: 'already-disabled@outlook.com', user_disabled: 1, tag_id: 0, tag_name: '' },
{ id: 4, email_address: 'unused@outlook.com', user_disabled: 0, tag_id: 0, tag_name: '' },
];
}
async function getState() {
return {
currentLuckmailPurchase: { id: 1 },
mailProvider: 'luckmail-api',
};
}
function getCurrentLuckmailPurchase(state) {
return state.currentLuckmailPurchase;
}
function isLuckmailProvider(state) {
return state.mailProvider === 'luckmail-api';
}
async function clearLuckmailRuntimeState(options) {
clearedOptions = options;
}
async function addLog() {}
${bundle}
return {
disableUsedLuckmailPurchases,
snapshot() {
return { disabledCalls, clearedOptions };
},
};
`);
const api = factory();
const result = await api.disableUsedLuckmailPurchases();
const snapshot = api.snapshot();
assert.deepStrictEqual(result.disabledIds, [1]);
assert.deepStrictEqual(snapshot.disabledCalls, [{ ids: [1], disabled: 1 }]);
assert.deepStrictEqual(snapshot.clearedOptions, { clearEmail: true });
});
test('resetState preserves LuckMail session config, used map, and preserve tag cache while clearing runtime purchase state', async () => {
const bundle = extractFunction('resetState');
const factory = new Function([
'let cleared = false;',
'let storedPayload = null;',
"const LOG_PREFIX = '[test]';",
"const DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME = '保留';",
'const DEFAULT_STATE = {',
" luckmailApiKey: '',",
" luckmailBaseUrl: 'https://mails.luckyous.com',",
" luckmailEmailType: 'ms_graph',",
" luckmailDomain: '',",
' luckmailUsedPurchases: {},',
' luckmailPreserveTagId: 0,',
" luckmailPreserveTagName: '保留',",
" currentLuckmailPurchase: { token: 'stale' },",
" currentLuckmailMailCursor: { messageId: 'stale' },",
' email: null,',
'};',
'function normalizeLuckmailBaseUrl(value) {',
" const normalized = String(value || '').trim() || 'https://mails.luckyous.com';",
" return normalized.replace(/\\/$/, '');",
'}',
'function normalizeLuckmailEmailType(value) {',
" return ['self_built', 'ms_imap', 'ms_graph', 'google_variant'].includes(String(value || '').trim())",
" ? String(value || '').trim()",
" : 'ms_graph';",
'}',
'function normalizeLuckmailUsedPurchases(value) {',
' return value || {};',
'}',
'async function getPersistedSettings() {',
" return { mailProvider: '163' };",
'}',
'async function getPersistedAliasState() {',
' return {};',
'}',
'const chrome = {',
' storage: {',
' session: {',
' async get() {',
' return {',
" seenCodes: ['seen-1'],",
" seenInbucketMailIds: ['mail-1'],",
" accounts: [{ email: 'saved@example.com' }],",
" tabRegistry: { foo: { tabId: 1 } },",
" sourceLastUrls: { foo: 'https://example.com' },",
" luckmailApiKey: 'sk-session',",
" luckmailBaseUrl: 'https://demo.example.com/',",
" luckmailEmailType: 'ms_imap',",
" luckmailDomain: 'outlook.com',",
" luckmailUsedPurchases: { 88: true },",
' luckmailPreserveTagId: 9,',
" luckmailPreserveTagName: '保留',",
' };',
' },',
' async clear() {',
' cleared = true;',
' },',
' async set(payload) {',
' storedPayload = payload;',
' },',
' },',
' },',
'};',
bundle,
'return {',
' resetState,',
' snapshot() {',
' return { cleared, storedPayload };',
' },',
'};',
].join('\n'));
const api = factory();
await api.resetState();
const snapshot = api.snapshot();
assert.equal(snapshot.cleared, true);
assert.equal(snapshot.storedPayload.luckmailApiKey, 'sk-session');
assert.equal(snapshot.storedPayload.luckmailBaseUrl, 'https://demo.example.com');
assert.equal(snapshot.storedPayload.luckmailEmailType, 'ms_imap');
assert.equal(snapshot.storedPayload.luckmailDomain, 'outlook.com');
assert.deepStrictEqual(snapshot.storedPayload.luckmailUsedPurchases, { 88: true });
assert.equal(snapshot.storedPayload.luckmailPreserveTagId, 9);
assert.equal(snapshot.storedPayload.luckmailPreserveTagName, '保留');
assert.equal(snapshot.storedPayload.currentLuckmailPurchase, null);
assert.equal(snapshot.storedPayload.currentLuckmailMailCursor, null);
});
test('handleStepData step 9 marks current LuckMail purchase as used and clears runtime state', async () => {
const bundle = extractFunction('handleStepData');
const factory = new Function(`
let clearedOptions = null;
let usedMarker = null;
const logs = [];
async function closeLocalhostCallbackTabs() {}
async function getState() {
return {
mailProvider: 'luckmail-api',
currentHotmailAccountId: null,
currentLuckmailPurchase: {
id: 123,
email_address: 'demo@outlook.com',
},
email: 'demo@outlook.com',
};
}
function getCurrentLuckmailPurchase(state) {
return state.currentLuckmailPurchase;
}
function isHotmailProvider() {
return false;
}
async function patchHotmailAccount() {}
function isLuckmailProvider(state) {
return state.mailProvider === 'luckmail-api';
}
async function setLuckmailPurchaseUsedState(purchaseId, used) {
usedMarker = { purchaseId, used };
}
async function clearLuckmailRuntimeState(options) {
clearedOptions = options;
}
async function addLog(message, level) {
logs.push({ message, level });
}
function buildLocalhostCleanupPrefix() {
return '';
}
async function closeTabsByUrlPrefix() {}
function shouldUseCustomRegistrationEmail() {
return false;
}
async function setEmailStateSilently() {}
async function setState() {}
function broadcastDataUpdate() {}
function isLocalhostOAuthCallbackUrl() {
return true;
}
async function finalizeIcloudAliasAfterSuccessfulFlow() {}
${bundle}
return {
handleStepData,
snapshot() {
return { clearedOptions, usedMarker, logs };
},
};
`);
const api = factory();
await api.handleStepData(9, {
localhostUrl: 'http://localhost:1455/auth/callback?code=abc&state=xyz',
});
const snapshot = api.snapshot();
assert.deepStrictEqual(snapshot.usedMarker, { purchaseId: 123, used: true });
assert.deepStrictEqual(snapshot.clearedOptions, { clearEmail: true });
assert.equal(snapshot.logs.at(-1).message, '当前 LuckMail 邮箱运行态已清空,下轮将优先复用未用邮箱或重新购买邮箱。');
});
@@ -0,0 +1,160 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const source = fs.readFileSync('background.js', 'utf8');
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
.map((marker) => source.indexOf(marker))
.find((index) => index >= 0);
if (start < 0) {
throw new Error(`missing function ${name}`);
}
let parenDepth = 0;
let signatureEnded = false;
let braceStart = -1;
for (let i = start; i < source.length; i += 1) {
const ch = source[i];
if (ch === '(') {
parenDepth += 1;
} else if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (ch === '{' && signatureEnded) {
braceStart = i;
break;
}
}
if (braceStart < 0) {
throw new Error(`missing body for function ${name}`);
}
let depth = 0;
let end = braceStart;
for (; end < source.length; end += 1) {
const ch = source[end];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return source.slice(start, end);
}
test('pollCloudflareTempEmailVerificationCode returns code even if delete fails', async () => {
const bundle = [
extractFunction('isStopError'),
extractFunction('throwIfStopped'),
extractFunction('summarizeCloudflareTempEmailMessagesForLog'),
extractFunction('pollCloudflareTempEmailVerificationCode'),
].join('\n');
const api = new Function(`
let stopRequested = false;
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
const CLOUDFLARE_TEMP_EMAIL_DEFAULT_PAGE_SIZE = 20;
const logs = [];
function normalizeCloudflareTempEmailAddress(value) {
return String(value || '').trim().toLowerCase();
}
async function addLog(message, level) {
logs.push({ message, level });
}
async function sleepWithStop() {}
async function listCloudflareTempEmailMessages() {
return {
config: {},
messages: [{
id: 'mail-1',
address: 'user@example.com',
receivedDateTime: '2026-04-13T09:20:00.000Z',
subject: 'OpenAI verification code',
from: { emailAddress: { address: 'noreply@tm.openai.com' } },
bodyPreview: 'Your verification code is 123456.',
}],
};
}
function pickVerificationMessageWithTimeFallback(messages) {
return {
match: {
code: '123456',
receivedAt: Date.parse(messages[0].receivedDateTime),
message: messages[0],
},
usedRelaxedFilters: false,
usedTimeFallback: false,
};
}
async function deleteCloudflareTempEmailMail() {
throw new Error('delete failed');
}
${bundle}
return {
pollCloudflareTempEmailVerificationCode,
snapshot() {
return { logs };
},
};
`)();
const result = await api.pollCloudflareTempEmailVerificationCode(4, { email: 'user@example.com' }, {
targetEmail: 'user@example.com',
maxAttempts: 1,
intervalMs: 1,
});
assert.equal(result.code, '123456');
const state = api.snapshot();
assert.equal(state.logs.some((entry) => entry.message.includes('删除 Cloudflare Temp Email 邮件失败')), true);
});
test('pollCloudflareTempEmailVerificationCode requires target email', async () => {
const bundle = [
extractFunction('isStopError'),
extractFunction('throwIfStopped'),
extractFunction('pollCloudflareTempEmailVerificationCode'),
].join('\n');
const api = new Function(`
let stopRequested = false;
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
const CLOUDFLARE_TEMP_EMAIL_DEFAULT_PAGE_SIZE = 20;
function normalizeCloudflareTempEmailAddress(value) {
return String(value || '').trim().toLowerCase();
}
async function addLog() {}
async function sleepWithStop() {}
async function listCloudflareTempEmailMessages() {
throw new Error('should not reach list');
}
function pickVerificationMessageWithTimeFallback() {
return { match: null, usedRelaxedFilters: false, usedTimeFallback: false };
}
async function deleteCloudflareTempEmailMail() {}
function summarizeCloudflareTempEmailMessagesForLog() {
return '';
}
${bundle}
return { pollCloudflareTempEmailVerificationCode };
`)();
await assert.rejects(
api.pollCloudflareTempEmailVerificationCode(4, {}, {}),
/缺少目标邮箱地址/
);
});
+107
View File
@@ -0,0 +1,107 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const {
buildCloudflareTempEmailHeaders,
getCloudflareTempEmailAddressFromResponse,
normalizeCloudflareTempEmailBaseUrl,
normalizeCloudflareTempEmailDomain,
normalizeCloudflareTempEmailDomains,
normalizeCloudflareTempEmailMailApiMessages,
} = require('../cloudflare-temp-email-utils.js');
test('normalizeCloudflareTempEmailBaseUrl normalizes host and preserves path', () => {
assert.equal(
normalizeCloudflareTempEmailBaseUrl('temp.example.com/api/'),
'https://temp.example.com/api'
);
assert.equal(
normalizeCloudflareTempEmailBaseUrl('http://127.0.0.1:8787'),
'http://127.0.0.1:8787'
);
assert.equal(normalizeCloudflareTempEmailBaseUrl('::::'), '');
});
test('normalizeCloudflareTempEmailDomain and domains de-duplicate valid entries', () => {
assert.equal(normalizeCloudflareTempEmailDomain('@Mail.Example.com'), 'mail.example.com');
assert.equal(normalizeCloudflareTempEmailDomain('not-a-domain'), '');
assert.deepEqual(
normalizeCloudflareTempEmailDomains(['mail.example.com', 'MAIL.EXAMPLE.COM', 'bad-value']),
['mail.example.com']
);
});
test('buildCloudflareTempEmailHeaders includes auth headers and content type when needed', () => {
assert.deepEqual(
buildCloudflareTempEmailHeaders(
{
adminAuth: 'admin-secret',
customAuth: 'site-secret',
},
{ json: true }
),
{
'x-admin-auth': 'admin-secret',
'x-custom-auth': 'site-secret',
'Content-Type': 'application/json',
Accept: 'application/json',
}
);
});
test('normalizeCloudflareTempEmailMailApiMessages extracts sender, subject, code, and address from raw mime', () => {
const messages = normalizeCloudflareTempEmailMailApiMessages({
data: [
{
id: 'mail-1',
address: 'user@example.com',
created_at: '2026-04-13T09:15:00.000Z',
raw: [
'From: OpenAI <noreply@tm.openai.com>',
'Subject: =?UTF-8?B?T3BlbkFJIHZlcmlmaWNhdGlvbiBjb2Rl?=',
'Content-Type: text/plain; charset=UTF-8',
'',
'Your verification code is 654321.',
].join('\r\n'),
},
],
});
assert.equal(messages.length, 1);
assert.equal(messages[0].id, 'mail-1');
assert.equal(messages[0].address, 'user@example.com');
assert.equal(messages[0].subject, 'OpenAI verification code');
assert.equal(messages[0].from.emailAddress.address, 'OpenAI <noreply@tm.openai.com>');
assert.match(messages[0].bodyPreview, /654321/);
});
test('normalizeCloudflareTempEmailMailApiMessages decodes multipart quoted printable html bodies', () => {
const messages = normalizeCloudflareTempEmailMailApiMessages([
{
id: 'mail-2',
address: 'user@example.com',
received_at: '2026-04-13T09:20:00.000Z',
source: [
'From: ChatGPT <noreply@tm.openai.com>',
'Subject: Login code',
'Content-Type: multipart/alternative; boundary="abc123"',
'',
'--abc123',
'Content-Type: text/html; charset=UTF-8',
'Content-Transfer-Encoding: quoted-printable',
'',
'<p>Your login code is <strong>112233</strong>.</p>',
'--abc123--',
].join('\r\n'),
},
]);
assert.equal(messages.length, 1);
assert.match(messages[0].bodyPreview, /112233/);
assert.equal(messages[0].subject, 'Login code');
});
test('getCloudflareTempEmailAddressFromResponse supports direct and nested response shapes', () => {
assert.equal(getCloudflareTempEmailAddressFromResponse({ address: 'one@example.com' }), 'one@example.com');
assert.equal(getCloudflareTempEmailAddressFromResponse({ data: { address: 'two@example.com' } }), 'two@example.com');
});
+15 -1
View File
@@ -26,6 +26,20 @@ test('Hotmail API对接应接入微软邮箱 helper 而不是旧远程服务占
const background = fs.readFileSync('background.js', 'utf8');
assert.match(background, /importScripts\([^)]*microsoft-email\.js[^)]*\)/, 'background 应加载微软邮箱 helper');
assert.match(background, /fetchMicrosoftVerificationCode/, '步骤 4\/7 应接入微软验证码读取 helper');
assert.match(background, /fetchMicrosoftMailboxMessages/, '账号校验和最新验证码应接入微软邮箱列表 helper');
assert.match(
background,
/fetchMicrosoftMailboxMessages\(\{[\s\S]*mailbox,[\s\S]*top:\s*10,/,
'微软邮箱 helper 应按请求的邮箱夹读取消息'
);
assert.match(
background,
/for \(const mailbox of mailboxes\) \{\s*const result = await requestHotmailRemoteMailbox\(workingAccount, mailbox\);/s,
'API对接模式不应丢掉 INBOX\/Junk 的逐邮箱夹轮询'
);
assert.match(
background,
/pickVerificationMessageWithTimeFallback\(fetchResult\.messages, \{/,
'步骤 4\/7 应继续复用现有验证码筛选与时间回退逻辑'
);
});
+121
View File
@@ -0,0 +1,121 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const {
findIcloudAliasArray,
findIcloudAliasByEmail,
getConfiguredIcloudHostPreference,
getIcloudHostHintFromMessage,
getIcloudLoginUrlForHost,
getIcloudSetupUrlForHost,
normalizeBooleanMap,
normalizeIcloudAliasList,
normalizeIcloudAliasRecord,
normalizeIcloudHost,
pickReusableIcloudAlias,
toNormalizedEmailSet,
} = require('../icloud-utils.js');
test('normalizeIcloudHost and host preference helpers resolve supported hosts', () => {
assert.equal(normalizeIcloudHost('www.icloud.com'), 'icloud.com');
assert.equal(normalizeIcloudHost('setup.icloud.com.cn'), 'icloud.com.cn');
assert.equal(normalizeIcloudHost('example.com'), '');
assert.equal(getConfiguredIcloudHostPreference({ icloudHostPreference: 'icloud.com' }), 'icloud.com');
assert.equal(getConfiguredIcloudHostPreference({ icloudHostPreference: 'auto' }), '');
assert.equal(getIcloudLoginUrlForHost('icloud.com.cn'), 'https://www.icloud.com.cn/');
assert.equal(getIcloudSetupUrlForHost('icloud.com'), 'https://setup.icloud.com/setup/ws/1');
});
test('getIcloudHostHintFromMessage can infer host from error text', () => {
assert.equal(getIcloudHostHintFromMessage('status 401 from setup.icloud.com.cn/setup/ws/1'), 'icloud.com.cn');
assert.equal(getIcloudHostHintFromMessage('request failed at https://www.icloud.com/'), 'icloud.com');
assert.equal(getIcloudHostHintFromMessage('unknown host'), '');
});
test('findIcloudAliasArray finds nested alias collections', () => {
const payload = {
result: {
data: {
items: [
{ hme: 'first@icloud.com', anonymousId: 'a1' },
{ hme: 'second@icloud.com', anonymousId: 'a2' },
],
},
},
};
assert.deepEqual(findIcloudAliasArray(payload), payload.result.data.items);
});
test('normalizeIcloudAliasRecord merges used and preserved state', () => {
const alias = normalizeIcloudAliasRecord({
anonymousId: 'alias-1',
hme: 'Demo@iCloud.com',
label: 'Test',
note: 'Created by test',
state: 'active',
}, {
usedEmails: ['demo@icloud.com'],
preservedEmails: ['demo@icloud.com'],
});
assert.deepEqual(alias, {
anonymousId: 'alias-1',
email: 'demo@icloud.com',
label: 'Test',
note: 'Created by test',
state: 'active',
active: true,
used: true,
preserved: true,
createdAt: null,
});
});
test('normalizeIcloudAliasList orders active unused aliases before used aliases', () => {
const aliases = normalizeIcloudAliasList({
hmeEmails: [
{ hme: 'used@icloud.com', anonymousId: 'u1', active: true },
{ hme: 'fresh@icloud.com', anonymousId: 'f1', active: true },
{ hme: 'inactive@icloud.com', anonymousId: 'i1', active: false },
],
}, {
usedEmails: ['used@icloud.com'],
preservedEmails: ['inactive@icloud.com'],
});
assert.deepEqual(aliases.map((alias) => alias.email), [
'fresh@icloud.com',
'used@icloud.com',
'inactive@icloud.com',
]);
assert.equal(aliases[2].preserved, true);
});
test('pickReusableIcloudAlias and findIcloudAliasByEmail select expected aliases', () => {
const aliases = normalizeIcloudAliasList({
hmeEmails: [
{ hme: 'used@icloud.com', anonymousId: 'u1', active: true },
{ hme: 'fresh@icloud.com', anonymousId: 'f1', active: true },
],
}, {
usedEmails: ['used@icloud.com'],
});
assert.equal(pickReusableIcloudAlias(aliases)?.email, 'fresh@icloud.com');
assert.equal(findIcloudAliasByEmail(aliases, 'FRESH@ICLOUD.COM')?.anonymousId, 'f1');
});
test('normalizeBooleanMap and toNormalizedEmailSet normalize keys and truthy entries', () => {
const normalized = normalizeBooleanMap({
' Demo@icloud.com ': 1,
'skip@icloud.com': 0,
});
assert.deepEqual(normalized, {
'demo@icloud.com': true,
'skip@icloud.com': false,
});
assert.deepEqual([...toNormalizedEmailSet(normalized)], ['demo@icloud.com']);
});
+261
View File
@@ -0,0 +1,261 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const {
DEFAULT_LUCKMAIL_BASE_URL,
DEFAULT_LUCKMAIL_EMAIL_TYPE,
DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME,
buildLuckmailBaselineCursor,
buildLuckmailMailCursor,
extractLuckmailVerificationCode,
filterReusableLuckmailPurchases,
isLuckmailMailNewerThanCursor,
isLuckmailPurchaseForProject,
normalizeLuckmailBaseUrl,
normalizeLuckmailEmailType,
normalizeLuckmailProjectName,
normalizeLuckmailPurchaseListPage,
normalizeLuckmailTags,
normalizeLuckmailUsedPurchases,
normalizeTimestamp,
normalizeLuckmailTokenCode,
normalizeLuckmailTokenMail,
pickReusableLuckmailPurchase,
pickLuckmailVerificationMail,
} = require('../luckmail-utils.js');
test('normalizeLuckmailEmailType keeps supported values and falls back to ms_graph', () => {
assert.equal(normalizeLuckmailEmailType('self_built'), 'self_built');
assert.equal(normalizeLuckmailEmailType('ms_imap'), 'ms_imap');
assert.equal(normalizeLuckmailEmailType('ms_graph'), 'ms_graph');
assert.equal(normalizeLuckmailEmailType('google_variant'), 'google_variant');
assert.equal(normalizeLuckmailEmailType(''), DEFAULT_LUCKMAIL_EMAIL_TYPE);
assert.equal(normalizeLuckmailEmailType('unknown'), DEFAULT_LUCKMAIL_EMAIL_TYPE);
});
test('normalizeLuckmailBaseUrl trims invalid input to default base url', () => {
assert.equal(normalizeLuckmailBaseUrl(''), DEFAULT_LUCKMAIL_BASE_URL);
assert.equal(normalizeLuckmailBaseUrl('https://mails.luckyous.com/'), DEFAULT_LUCKMAIL_BASE_URL);
assert.equal(normalizeLuckmailBaseUrl('https://demo.example.com/api/'), 'https://demo.example.com/api');
assert.equal(normalizeLuckmailBaseUrl('notaurl'), DEFAULT_LUCKMAIL_BASE_URL);
});
test('normalizeLuckmailTokenCode and normalizeLuckmailTokenMail extract verification code', () => {
const tokenCode = normalizeLuckmailTokenCode({
email_address: 'demo@outlook.com',
project: 'openai',
has_new_mail: true,
verification_code: '123456',
mail: {
message_id: 'mail-1',
from: 'noreply@openai.com',
subject: 'Your ChatGPT code is 123456',
received_at: '2026-04-14T10:00:00Z',
},
});
assert.equal(tokenCode.verification_code, '123456');
assert.equal(tokenCode.mail.message_id, 'mail-1');
assert.equal(tokenCode.mail.verification_code, '123456');
const normalizedMail = normalizeLuckmailTokenMail({
message_id: 'mail-2',
from: 'noreply@openai.com',
subject: 'OpenAI security message',
body: 'Your verification code is 654321.',
received_at: '2026-04-14T10:01:00Z',
});
assert.equal(normalizedMail.verification_code, '654321');
assert.equal(extractLuckmailVerificationCode('你的验证码为 778899'), '778899');
});
test('normalizeLuckmailProjectName and isLuckmailPurchaseForProject match openai case-insensitively', () => {
assert.equal(normalizeLuckmailProjectName(' OpenAi '), 'openai');
assert.equal(isLuckmailPurchaseForProject({
id: 1,
email_address: 'demo@outlook.com',
token: 'tok-1',
project_name: 'OpenAi',
}, 'openai'), true);
assert.equal(isLuckmailPurchaseForProject({
id: 2,
email_address: 'other@example.com',
token: 'tok-2',
project: 'OtherProject',
}, 'openai'), false);
});
test('normalizeLuckmailPurchaseListPage and normalizeLuckmailTags normalize list payloads', () => {
const page = normalizeLuckmailPurchaseListPage({
list: [{
id: 3,
email_address: 'demo@outlook.com',
token: 'tok-3',
project_name: 'OpenAi',
}],
total: 4,
page: 2,
page_size: 1,
});
assert.equal(page.total, 4);
assert.equal(page.page, 2);
assert.equal(page.page_size, 1);
assert.equal(page.list[0].project_code, 'openai');
const tags = normalizeLuckmailTags([{
id: 9,
name: DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME,
limit_type: 0,
}]);
assert.deepEqual(tags[0], {
id: 9,
name: DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME,
remark: '',
limit_type: 0,
purchase_count: 0,
created_at: null,
});
});
test('normalizeLuckmailUsedPurchases keeps positive numeric keys only', () => {
assert.deepEqual(normalizeLuckmailUsedPurchases({
1: true,
foo: true,
'-2': true,
3: false,
}), {
1: true,
3: false,
});
});
test('pickReusableLuckmailPurchase only returns reusable openai purchase', () => {
const purchases = [{
id: 10,
email_address: 'used@outlook.com',
token: 'tok-used',
project_name: 'openai',
}, {
id: 11,
email_address: 'preserved@outlook.com',
token: 'tok-preserved',
project_name: 'OpenAi',
tag_name: DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME,
}, {
id: 12,
email_address: 'disabled@outlook.com',
token: 'tok-disabled',
project_name: 'openai',
user_disabled: 1,
}, {
id: 13,
email_address: 'expired@outlook.com',
token: 'tok-expired',
project_name: 'openai',
warranty_until: '2026-04-14T09:00:00Z',
}, {
id: 14,
email_address: 'other@example.com',
token: 'tok-other',
project_name: 'other',
}, {
id: 15,
email_address: 'ready@outlook.com',
token: 'tok-ready',
project_name: 'OpenAi',
warranty_until: '2026-04-15T09:00:00Z',
}];
const reusable = filterReusableLuckmailPurchases(purchases, {
projectCode: 'openai',
usedPurchases: { 10: true },
preserveTagName: DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME,
now: Date.parse('2026-04-14T10:00:00Z'),
});
assert.deepEqual(reusable.map((purchase) => purchase.id), [15]);
assert.equal(pickReusableLuckmailPurchase(purchases, {
projectCode: 'openai',
usedPurchases: { 10: true },
preserveTagName: DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME,
now: Date.parse('2026-04-14T10:00:00Z'),
}).id, 15);
});
test('pickLuckmailVerificationMail respects sender filters, time filters, and excluded codes', () => {
const match = pickLuckmailVerificationMail([
{
message_id: 'old-mail',
from: 'noreply@openai.com',
subject: 'Your code is 111111',
received_at: '2026-04-14T09:59:00Z',
},
{
message_id: 'new-mail',
from: 'noreply@openai.com',
subject: 'Your code is 222222',
received_at: '2026-04-14T10:05:00Z',
},
], {
senderFilters: ['openai'],
subjectFilters: ['code'],
excludeCodes: ['111111'],
afterTimestamp: Date.parse('2026-04-14T10:00:00Z'),
});
assert.equal(match.code, '222222');
assert.equal(match.mail.message_id, 'new-mail');
});
test('isLuckmailMailNewerThanCursor compares message id and timestamp safely', () => {
const cursor = buildLuckmailMailCursor({
message_id: 'mail-1',
received_at: '2026-04-14T10:00:00Z',
});
assert.equal(isLuckmailMailNewerThanCursor({
message_id: 'mail-1',
received_at: '2026-04-14T10:00:00Z',
}, cursor), false);
assert.equal(isLuckmailMailNewerThanCursor({
message_id: 'mail-2',
received_at: '2026-04-14T10:01:00Z',
}, cursor), true);
});
test('normalizeLuckmailMailCursor tolerates null cursor input', () => {
const { normalizeLuckmailMailCursor } = require('../luckmail-utils.js');
assert.deepEqual(normalizeLuckmailMailCursor(null), {
messageId: '',
receivedAt: '',
});
});
test('normalizeTimestamp treats LuckMail naive datetime strings as UTC', () => {
assert.equal(
normalizeTimestamp('2026-04-14 13:32:05'),
Date.UTC(2026, 3, 14, 13, 32, 5, 0)
);
});
test('buildLuckmailBaselineCursor tracks newest existing mail as baseline', () => {
const cursor = buildLuckmailBaselineCursor([
{
message_id: 'mail-old',
received_at: '2026-04-14 13:31:15',
subject: '你的 ChatGPT 代码为 111111',
},
{
message_id: 'mail-new',
received_at: '2026-04-14 13:32:05',
subject: '你的 ChatGPT 代码为 222222',
},
]);
assert.deepEqual(cursor, {
messageId: 'mail-new',
receivedAt: '2026-04-14 13:32:05',
});
});
+81 -13
View File
@@ -5,9 +5,10 @@ const {
extractVerificationCodeFromMessages,
fetchMicrosoftMailboxMessages,
fetchMicrosoftVerificationCode,
normalizeMailboxId,
} = require('../microsoft-email.js');
test('extractVerificationCodeFromMessages 只命中过滤时间后的 OpenAI 微软邮件', () => {
test('extractVerificationCodeFromMessages 支持显式过滤条件并跳过排除的验证码', () => {
const result = extractVerificationCodeFromMessages([
{
From: { EmailAddress: { Address: 'noreply@openai.com' } },
@@ -32,6 +33,9 @@ test('extractVerificationCodeFromMessages 只命中过滤时间后的 OpenAI 微
},
], {
filterAfterTimestamp: Date.UTC(2026, 3, 14, 9, 30, 0),
senderFilters: ['openai'],
subjectFilters: ['verification'],
excludeCodes: ['112233'],
});
assert.deepEqual(result, {
@@ -40,14 +44,46 @@ test('extractVerificationCodeFromMessages 只命中过滤时间后的 OpenAI 微
messageId: 'matched',
sender: 'account-security@openai.com',
subject: 'OpenAI verification',
mailbox: 'INBOX',
message: {
mailbox: 'INBOX',
from: {
emailAddress: {
address: 'account-security@openai.com',
name: '',
},
},
subject: 'OpenAI verification',
receivedDateTime: '2026-04-14T10:05:00.000Z',
bodyPreview: 'Use 334455 to continue',
body: {
content: '',
},
id: 'matched',
},
});
});
test('fetchMicrosoftMailboxMessages 使用 refresh token 拉取微软邮箱列表并返回新 token', async () => {
test('normalizeMailboxId 将 Junk 归一为微软邮箱夹 ID', () => {
assert.equal(normalizeMailboxId('INBOX'), 'inbox');
assert.equal(normalizeMailboxId('junk'), 'junkemail');
assert.equal(normalizeMailboxId('Junk Email'), 'junkemail');
});
test('fetchMicrosoftMailboxMessages 会回退到可用的 token 策略并保留邮箱夹信息', async () => {
const requests = [];
const fetchImpl = async (url, options = {}) => {
requests.push({ url, options });
if (String(url).includes('/oauth2/v2.0/token')) {
const params = new URLSearchParams(String(options.body || ''));
if (String(url).includes('/common/') && params.get('scope')?.includes('Mail.Read')) {
return {
ok: false,
status: 400,
statusText: 'Bad Request',
text: async () => JSON.stringify({ error_description: 'common delegated failed' }),
};
}
return {
ok: true,
json: async () => ({
@@ -57,16 +93,17 @@ test('fetchMicrosoftMailboxMessages 使用 refresh token 拉取微软邮箱列
};
}
assert.match(String(url), /graph\.microsoft\.com\/v1\.0\/me\/mailFolders\/junkemail\/messages/);
return {
ok: true,
json: async () => ({
value: [
{
From: { EmailAddress: { Address: 'noreply@openai.com' } },
Subject: 'OpenAI verification',
BodyPreview: 'Use 445566 to continue',
ReceivedDateTime: '2026-04-14T10:06:00.000Z',
Id: 'mail-1',
from: { emailAddress: { address: 'noreply@openai.com' } },
subject: 'OpenAI verification',
bodyPreview: 'Use 445566 to continue',
receivedDateTime: '2026-04-14T10:06:00.000Z',
id: 'mail-1',
},
],
}),
@@ -76,18 +113,25 @@ test('fetchMicrosoftMailboxMessages 使用 refresh token 拉取微软邮箱列
const result = await fetchMicrosoftMailboxMessages({
clientId: 'client-1',
refreshToken: 'refresh-token-1',
mailbox: 'Junk',
top: 5,
fetchImpl,
});
assert.equal(requests.length, 2);
assert.equal(requests.length, 3);
assert.equal(result.nextRefreshToken, 'refresh-token-next');
assert.equal(result.tokenStrategy, 'entra-consumers-delegated');
assert.equal(result.transport, 'graph');
assert.equal(result.messages.length, 1);
assert.equal(result.messages[0].id, 'mail-1');
assert.equal(result.messages[0].mailbox, 'Junk');
});
test('fetchMicrosoftVerificationCode 会重试直到命中最新验证码', async () => {
let messageRequestCount = 0;
test('fetchMicrosoftVerificationCode 会按邮箱夹轮询并在 Junk 中命中最新验证码', async () => {
const mailboxRequests = {
inbox: 0,
junkemail: 0,
};
const logs = [];
const fetchImpl = async (url) => {
if (String(url).includes('/oauth2/v2.0/token')) {
@@ -100,8 +144,9 @@ test('fetchMicrosoftVerificationCode 会重试直到命中最新验证码', asyn
};
}
messageRequestCount += 1;
if (messageRequestCount === 1) {
const urlString = String(url);
if (urlString.includes('/mailFolders/inbox/messages')) {
mailboxRequests.inbox += 1;
return {
ok: true,
json: async () => ({
@@ -116,11 +161,28 @@ test('fetchMicrosoftVerificationCode 会重试直到命中最新验证码', asyn
};
}
assert.match(urlString, /mailFolders\/junkemail\/messages/);
mailboxRequests.junkemail += 1;
if (mailboxRequests.junkemail === 1) {
return {
ok: true,
json: async () => ({
value: [{
from: { emailAddress: { address: 'no-reply@example.com' } },
subject: 'Nothing useful',
bodyPreview: 'Still no code',
receivedDateTime: '2026-04-14T10:05:00.000Z',
id: 'mail-ignore-2',
}],
}),
};
}
return {
ok: true,
json: async () => ({
value: [{
From: { EmailAddress: { Address: 'noreply@openai.com' } },
from: { emailAddress: { address: 'account-security@openai.com' } },
Subject: 'Your verification code',
BodyPreview: '667788',
ReceivedDateTime: '2026-04-14T10:10:00.000Z',
@@ -135,13 +197,19 @@ test('fetchMicrosoftVerificationCode 会重试直到命中最新验证码', asyn
clientId: 'client-2',
maxRetries: 2,
retryDelayMs: 0,
mailboxes: ['INBOX', 'Junk'],
fetchImpl,
log: (message) => logs.push(message),
filterAfterTimestamp: Date.UTC(2026, 3, 14, 9, 0, 0),
senderFilters: ['openai'],
subjectFilters: ['verification'],
});
assert.equal(result.code, '667788');
assert.equal(result.messageId, 'mail-hit');
assert.equal(result.nextRefreshToken, 'refresh-token-next-2');
assert.equal(result.mailbox, 'Junk');
assert.equal(mailboxRequests.inbox, 2);
assert.equal(mailboxRequests.junkemail, 2);
assert.equal(logs.some((message) => /retrying/i.test(message)), true);
});
@@ -52,7 +52,14 @@ function extractFunction(name) {
const bundle = [
extractFunction('getTabRegistry'),
extractFunction('normalizeEmailGenerator'),
extractFunction('normalizeMail2925Mode'),
extractFunction('getMail2925Mode'),
extractFunction('parseUrlSafely'),
extractFunction('isHotmailProvider'),
extractFunction('isCustomMailProvider'),
extractFunction('isGeneratedAliasProvider'),
extractFunction('shouldUseCustomRegistrationEmail'),
extractFunction('isLocalhostOAuthCallbackUrl'),
extractFunction('isLocalhostOAuthCallbackTabMatch'),
extractFunction('closeLocalhostCallbackTabs'),
@@ -62,6 +69,12 @@ const bundle = [
].join('\n');
const api = new Function(`
const HOTMAIL_PROVIDER = 'hotmail-api';
const CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email';
const CLOUDFLARE_TEMP_EMAIL_GENERATOR = 'cloudflare-temp-email';
const MAIL_2925_MODE_PROVIDE = 'provide';
const MAIL_2925_MODE_RECEIVE = 'receive';
const DEFAULT_MAIL_2925_MODE = MAIL_2925_MODE_PROVIDE;
let currentState = {
tabRegistry: {
'signup-page': { tabId: 1, ready: true },
@@ -96,12 +109,37 @@ async function setEmailState(email) {
currentState = { ...currentState, email };
}
async function setEmailStateSilently(email) {
currentState = { ...currentState, email };
}
function isHotmailProvider() {
return false;
}
function isLuckmailProvider() {
return false;
}
async function patchHotmailAccount() {}
async function clearLuckmailRuntimeState() {}
function shouldUseCustomRegistrationEmail() {
return false;
}
function broadcastDataUpdate() {}
async function addLog(message) {
logMessages.push(message);
}
async function finalizeIcloudAliasAfterSuccessfulFlow() {}
function shouldUseCustomRegistrationEmail() {
return false;
}
${bundle}
return {
+9 -2
View File
@@ -57,10 +57,12 @@ async function testPollFreshVerificationCodeRethrowsStop() {
extractFunction('pollFreshVerificationCode'),
].join('\n');
const api = new Function(`
const api = new Function(`
let stopRequested = false;
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
const HOTMAIL_PROVIDER = 'hotmail-api';
const LUCKMAIL_PROVIDER = 'luckmail-api';
const CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email';
const VERIFICATION_POLL_MAX_ROUNDS = 5;
const logs = [];
let resendCalls = 0;
@@ -71,6 +73,9 @@ function getHotmailVerificationPollConfig() {
async function pollHotmailVerificationCode() {
throw new Error('hotmail path should not run in this test');
}
async function pollLuckmailVerificationCode() {
throw new Error('luckmail path should not run in this test');
}
function getVerificationCodeStateKey(step) {
return step === 4 ? 'lastSignupCode' : 'lastLoginCode';
}
@@ -119,9 +124,11 @@ async function testResolveVerificationStepRethrowsStopFromFreshRequest() {
extractFunction('resolveVerificationStep'),
].join('\n');
const api = new Function(`
const api = new Function(`
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
const HOTMAIL_PROVIDER = 'hotmail-api';
const LUCKMAIL_PROVIDER = 'luckmail-api';
const CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email';
const logs = [];
let pollCalls = 0;