| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680 |
- import assert from 'node:assert/strict';
- import { createHash } from 'node:crypto';
- import { mkdtempSync } from 'node:fs';
- import { tmpdir } from 'node:os';
- import path from 'node:path';
- import { test } from 'node:test';
- import { DatabaseSync } from 'node:sqlite';
- import {
- authenticateUser,
- claimLegacyData,
- consumeAccountToken,
- createApiToken,
- createAccountToken,
- createSendEvent,
- createTrackingLink,
- createDomain,
- createUser,
- createUserWithAccountToken,
- createWebhook,
- deleteSmtpCredential,
- deleteSmtpRelay,
- getDnsCredential,
- getDomain,
- getSendEvent,
- getSendAnalytics,
- getSmtpRelay,
- getSmtpCredential,
- getAdminResourceInventory,
- getSystemEmailSettings,
- getUser,
- initDatabase,
- listAuditLogs,
- listDomains,
- listSendEvents,
- listSmtpCredentials,
- listSmtpRelays,
- listUsersWithResourceCounts,
- invalidateAccountTokens,
- logAudit,
- logSendEvent,
- approveUser,
- markUserEmailVerified,
- previewUserMerge,
- pruneTrackingEvents,
- recordTrackingEvent,
- saveDnsCredential,
- saveSmtpRelay,
- saveSmtpCredential,
- saveSystemEmailSettings,
- seedAdminUser,
- transferApiTokens,
- transferDnsCredential,
- transferDomain,
- updateUser,
- updateUserStatus,
- executeUserMerge,
- finalizeSendEvent,
- findSendEventByTrackingToken,
- findTrackingLinkByToken,
- verifyApiToken,
- verifySmtpCredential
- } from '../src/db.js';
- import {
- createTrackingToken,
- encryptTrackingTarget,
- hashTrackingToken,
- trackingTargetFingerprint
- } from '../src/tracking.js';
- test('migrates legacy data to the seeded admin user', () => {
- const dataDir = tempDataDir();
- const dbPath = path.join(dataDir, 'mailhub.sqlite');
- const legacy = new DatabaseSync(dbPath);
- legacy.exec(`
- CREATE TABLE domains (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- domain TEXT NOT NULL UNIQUE,
- selector TEXT NOT NULL,
- verification_token TEXT NOT NULL,
- dkim_public TEXT NOT NULL,
- dkim_private TEXT NOT NULL,
- sender_host TEXT NOT NULL,
- sending_ip TEXT NOT NULL,
- spf_extra TEXT NOT NULL DEFAULT '',
- dmarc_policy TEXT NOT NULL DEFAULT 'none',
- dmarc_rua TEXT NOT NULL DEFAULT '',
- status_json TEXT NOT NULL DEFAULT '{}',
- created_at TEXT NOT NULL,
- updated_at TEXT NOT NULL
- );
- CREATE TABLE send_events (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- domain_id INTEGER,
- sender TEXT NOT NULL,
- recipients TEXT NOT NULL,
- subject TEXT NOT NULL,
- status TEXT NOT NULL,
- detail TEXT NOT NULL DEFAULT '',
- created_at TEXT NOT NULL
- );
- CREATE TABLE smtp_credentials (
- id INTEGER PRIMARY KEY CHECK (id = 1),
- username TEXT NOT NULL,
- password_hash TEXT NOT NULL,
- password_secret TEXT NOT NULL DEFAULT '',
- created_at TEXT NOT NULL,
- updated_at TEXT NOT NULL
- );
- `);
- legacy
- .prepare(`
- INSERT INTO domains (
- domain, selector, verification_token, dkim_public, dkim_private,
- sender_host, sending_ip, spf_extra, dmarc_policy, dmarc_rua, created_at, updated_at
- ) VALUES ('legacy.example', 'mh', 'tok', 'pub', 'priv', 'mail.legacy.example', '127.0.0.1', '', 'none', '', 'now', 'now')
- `)
- .run();
- legacy
- .prepare(`
- INSERT INTO send_events (domain_id, sender, recipients, subject, status, detail, created_at)
- VALUES (1, 'noreply@legacy.example', '["user@example.com"]', 'hi', 'queued', '', 'now')
- `)
- .run();
- legacy
- .prepare(`
- INSERT INTO smtp_credentials (id, username, password_hash, password_secret, created_at, updated_at)
- VALUES (1, 'legacy-smtp', 'scrypt$salt$hash', '', 'now', 'now')
- `)
- .run();
- legacy.close();
- initDatabase(dataDir, 'test-secret');
- const admin = seedAdminUser({ username: 'admin', email: 'admin@example.com', password: 'password123' });
- claimLegacyData(admin.id);
- assert.equal(listDomains(admin.id).length, 1);
- assert.equal(listSendEvents(admin.id).length, 1);
- assert.equal(getSmtpCredential(admin.id).username, 'legacy-smtp');
- });
- test('migrates legacy send events with engagement tracking disabled', () => {
- const dataDir = tempDataDir();
- const database = initDatabase(dataDir, 'test-secret');
- const columns = database.prepare('PRAGMA table_info(send_events)').all().map((column) => column.name);
- assert.equal(columns.includes('tracking_token_hash'), true);
- assert.equal(columns.includes('tracking_opens'), true);
- assert.equal(columns.includes('tracking_clicks'), true);
- const tables = database
- .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name IN ('tracking_links', 'tracking_events') ORDER BY name")
- .all()
- .map((row) => row.name);
- assert.deepEqual(tables, ['tracking_events', 'tracking_links']);
- });
- test('creates and finalizes one tracked send event with a unique open token', () => {
- initDatabase(tempDataDir(), 'test-secret');
- const user = createUser({ username: 'tracked', email: 'tracked@example.com', password: 'password123' });
- const domain = createDomain(user.id, domainFixture('tracked.example'));
- const token = createTrackingToken();
- const id = createSendEvent({
- userId: user.id,
- domainId: domain.id,
- sender: 'sender@tracked.example',
- recipients: ['reader@example.net'],
- subject: 'Tracked',
- trackingToken: token,
- trackingOpens: true,
- trackingClicks: true
- });
- const submitting = getSendEvent(user.id, id);
- assert.equal(submitting.status, 'submitting');
- assert.equal(submitting.tracking.enabled, true);
- assert.equal(submitting.tracking.opens, true);
- assert.equal(submitting.tracking.clicks, true);
- assert.equal(findSendEventByTrackingToken(token).id, id);
- const finalized = finalizeSendEvent(id, user.id, {
- status: 'queued',
- detail: 'queued as TRACK123',
- queueId: 'TRACK123',
- deliveryLog: [{ phase: 'queue', ok: true }]
- });
- assert.equal(finalized.status, 'queued');
- assert.equal(finalized.queueId, 'TRACK123');
- assert.throws(() => createSendEvent({
- userId: user.id,
- domainId: domain.id,
- sender: 'sender@tracked.example',
- recipients: ['second@example.net'],
- subject: 'Duplicate token',
- trackingToken: token,
- trackingOpens: true
- }), /unique/i);
- });
- test('stores encrypted opaque tracking links and isolates creation by owner', () => {
- initDatabase(tempDataDir(), 'test-secret');
- const alice = createUser({ username: 'track-alice', email: 'track-alice@example.com', password: 'password123' });
- const bob = createUser({ username: 'track-bob', email: 'track-bob@example.com', password: 'password123' });
- const domain = createDomain(alice.id, domainFixture('links.example'));
- const eventId = createSendEvent({
- userId: alice.id,
- domainId: domain.id,
- sender: 'sender@links.example',
- recipients: ['reader@example.net'],
- subject: 'Links',
- trackingClicks: true
- });
- const token = createTrackingToken();
- const target = 'https://example.net/reset?token=private';
- const linkId = createTrackingLink(alice.id, eventId, {
- token,
- targetCiphertext: encryptTrackingTarget(target, 'tracking-secret'),
- targetFingerprint: hashTrackingToken(`destination:${target}`),
- targetOrigin: 'https://example.net'
- });
- const link = findTrackingLinkByToken(token);
- assert.equal(link.id, linkId);
- assert.equal(link.sendEventId, eventId);
- assert.equal(link.userId, alice.id);
- assert.equal(link.targetCiphertext.includes('private'), false);
- assert.equal(link.targetOrigin, 'https://example.net');
- const detail = getSendEvent(alice.id, eventId, { trackingSecret: 'tracking-secret' });
- assert.equal(detail.tracking.links.length, 1);
- assert.equal(detail.tracking.links[0].target, target);
- assert.throws(() => createTrackingLink(bob.id, eventId, {
- token: createTrackingToken(),
- targetCiphertext: encryptTrackingTarget('https://example.net/other', 'tracking-secret'),
- targetFingerprint: 'other',
- targetOrigin: 'https://example.net'
- }), /not found/i);
- });
- test('records deduplicated engagement events and summarizes message detail', () => {
- initDatabase(tempDataDir(), 'test-secret');
- const user = createUser({ username: 'events', email: 'events@example.com', password: 'password123' });
- const domain = createDomain(user.id, domainFixture('events.example'));
- const eventId = createSendEvent({
- userId: user.id,
- domainId: domain.id,
- sender: 'sender@events.example',
- recipients: ['reader@example.net'],
- subject: 'Engagement',
- trackingOpens: true,
- trackingClicks: true
- });
- const trackingLinkId = createTrackingLink(user.id, eventId, {
- token: createTrackingToken(),
- targetCiphertext: encryptTrackingTarget('https://example.net/click', 'tracking-secret'),
- targetFingerprint: trackingTargetFingerprint('https://example.net/click', 'tracking-secret'),
- targetOrigin: 'https://example.net'
- });
- const scanner = recordTrackingEvent({
- sendEventId: eventId,
- eventType: 'open',
- source: 'scanner',
- occurredAt: '2026-07-09T12:00:00.000Z',
- userAgent: 'Scanner',
- ipHash: 'scanner-ip',
- replayKey: 'scanner-open'
- });
- assert.equal(scanner.recorded, true);
- assert.equal(scanner.firstQualifying, false);
- const direct = recordTrackingEvent({
- sendEventId: eventId,
- eventType: 'open',
- source: 'direct',
- occurredAt: '2026-07-09T12:01:00.000Z',
- userAgent: 'Mozilla/5.0',
- ipHash: 'direct-ip',
- replayKey: 'direct-open'
- });
- assert.equal(direct.recorded, true);
- assert.equal(direct.firstQualifying, true);
- recordTrackingEvent({
- sendEventId: eventId,
- trackingLinkId,
- eventType: 'click',
- source: 'direct',
- occurredAt: '2026-07-09T11:59:00.000Z',
- userAgent: 'Mozilla/5.0',
- ipHash: 'click-ip',
- replayKey: 'direct-click'
- });
- const duplicate = recordTrackingEvent({
- sendEventId: eventId,
- eventType: 'open',
- source: 'direct',
- occurredAt: '2026-07-09T12:01:30.000Z',
- userAgent: 'Mozilla/5.0',
- ipHash: 'direct-ip',
- replayKey: 'direct-open'
- });
- assert.equal(duplicate.recorded, false);
- assert.equal(duplicate.duplicate, true);
- const detail = getSendEvent(user.id, eventId);
- assert.equal(detail.tracking.summary.totalOpens, 1);
- assert.equal(detail.tracking.summary.totalClicks, 1);
- assert.equal(detail.tracking.summary.uniqueOpen, true);
- assert.equal(detail.tracking.summary.scannerEvents, 1);
- assert.equal(detail.tracking.summary.firstOpenedAt, '2026-07-09T11:59:00.000Z');
- assert.equal(detail.tracking.summary.lastOpenedAt, '2026-07-09T12:01:00.000Z');
- assert.equal(detail.tracking.events.length, 3);
- assert.equal(detail.tracking.eventCount, 3);
- assert.equal(detail.tracking.eventsTruncated, false);
- assert.equal(detail.tracking.events.some((event) => Object.hasOwn(event, 'ipHash')), false);
- const listed = listSendEvents(user.id)[0];
- assert.equal(listed.tracking.summary.totalOpens, 1);
- assert.equal(listed.tracking.summary.uniqueOpen, true);
- });
- test('bounds tracking detail events while preserving the complete SQL summary', () => {
- const database = initDatabase(tempDataDir(), 'test-secret');
- const user = createUser({ username: 'bounded-events', email: 'bounded-events@example.com', password: 'password123' });
- const domain = createDomain(user.id, domainFixture('bounded-events.example'));
- const eventId = createSendEvent({
- userId: user.id,
- domainId: domain.id,
- sender: 'sender@bounded-events.example',
- recipients: ['reader@example.net'],
- subject: 'Bounded events',
- trackingOpens: true
- });
- const start = Date.parse('2026-07-09T00:00:00.000Z');
- for (let index = 0; index < 505; index += 1) {
- recordTrackingEvent({
- sendEventId: eventId,
- eventType: 'open',
- source: 'direct',
- occurredAt: new Date(start + index * 1000).toISOString(),
- userAgent: 'Mozilla/5.0',
- ipHash: `bounded-${index}`,
- replayKey: `bounded-${index}`
- });
- }
- for (let index = 0; index < 205; index += 1) {
- const target = `https://example.net/link/${index}`;
- createTrackingLink(user.id, eventId, {
- token: createTrackingToken(),
- targetCiphertext: encryptTrackingTarget(target, 'tracking-secret'),
- targetFingerprint: trackingTargetFingerprint(target, 'tracking-secret'),
- targetOrigin: 'https://example.net'
- });
- }
- const detail = getSendEvent(user.id, eventId, { trackingSecret: 'tracking-secret' });
- assert.equal(detail.tracking.summary.totalOpens, 505);
- assert.equal(detail.tracking.eventCount, 505);
- assert.equal(detail.tracking.events.length, 500);
- assert.equal(detail.tracking.eventsTruncated, true);
- assert.equal(detail.tracking.linkCount, 205);
- assert.equal(detail.tracking.links.length, 200);
- assert.equal(detail.tracking.linksTruncated, true);
- assert.equal(detail.tracking.events[0].occurredAt, '2026-07-09T00:00:05.000Z');
- assert.equal(detail.tracking.events.at(-1).occurredAt, '2026-07-09T00:08:24.000Z');
- const listed = listSendEvents(user.id)[0];
- assert.equal(listed.tracking.summary.totalOpens, 505);
- assert.equal(Object.hasOwn(listed.tracking, 'events'), false);
- const eventIndexes = database.prepare("PRAGMA index_list('tracking_events')").all().map((row) => row.name);
- const sendEventIndexes = database.prepare("PRAGMA index_list('send_events')").all().map((row) => row.name);
- assert.equal(eventIndexes.includes('idx_tracking_events_link_time'), true);
- assert.equal(sendEventIndexes.includes('idx_events_user_created'), true);
- });
- test('enqueues first qualifying engagement webhook and ignores scanner-first events', () => {
- initDatabase(tempDataDir(), 'test-secret');
- const user = createUser({ username: 'engagement-hook', email: 'engagement-hook@example.com', password: 'password123' });
- const domain = createDomain(user.id, domainFixture('engagement-hook.example'));
- createWebhook(user.id, {
- name: 'Engagement events',
- url: 'https://hooks.example.com/engagement',
- events: ['opened', 'clicked']
- });
- const eventId = createSendEvent({
- userId: user.id,
- domainId: domain.id,
- sender: 'sender@engagement-hook.example',
- recipients: ['reader@example.net'],
- subject: 'Webhook engagement',
- status: 'sent',
- trackingOpens: true,
- trackingClicks: true
- });
- recordTrackingEvent({
- sendEventId: eventId,
- eventType: 'open',
- source: 'scanner',
- occurredAt: '2026-07-09T12:00:00.000Z',
- userAgent: 'Scanner',
- ipHash: 'scanner-ip',
- replayKey: 'hook-scanner'
- });
- assert.equal(getSendEvent(user.id, eventId).webhookDeliveries.length, 0);
- recordTrackingEvent({
- sendEventId: eventId,
- eventType: 'open',
- source: 'direct',
- occurredAt: '2026-07-09T12:01:00.000Z',
- userAgent: 'Mozilla/5.0',
- ipHash: 'direct-ip',
- replayKey: 'hook-direct-1'
- });
- recordTrackingEvent({
- sendEventId: eventId,
- eventType: 'open',
- source: 'direct',
- occurredAt: '2026-07-09T12:02:00.000Z',
- userAgent: 'Mozilla/5.0',
- ipHash: 'direct-ip-2',
- replayKey: 'hook-direct-2'
- });
- const deliveries = getSendEvent(user.id, eventId).webhookDeliveries;
- assert.equal(deliveries.length, 1);
- assert.equal(deliveries[0].eventType, 'opened');
- });
- test('caps tracking writes per message and UTC day', () => {
- initDatabase(tempDataDir(), 'test-secret');
- const user = createUser({ username: 'cap-user', email: 'cap-user@example.com', password: 'password123' });
- const domain = createDomain(user.id, domainFixture('cap.example'));
- const eventId = createSendEvent({
- userId: user.id,
- domainId: domain.id,
- sender: 'sender@cap.example',
- recipients: ['reader@example.net'],
- subject: 'Cap',
- trackingOpens: true
- });
- for (let index = 0; index < 2; index += 1) {
- const result = recordTrackingEvent({
- sendEventId: eventId,
- eventType: 'open',
- source: 'direct',
- occurredAt: `2026-07-09T12:0${index}:00.000Z`,
- userAgent: 'Mozilla/5.0',
- ipHash: `ip-${index}`,
- replayKey: `cap-${index}`,
- maxPerDay: 2
- });
- assert.equal(result.recorded, true);
- }
- const capped = recordTrackingEvent({
- sendEventId: eventId,
- eventType: 'open',
- source: 'direct',
- occurredAt: '2026-07-09T12:03:00.000Z',
- userAgent: 'Mozilla/5.0',
- ipHash: 'ip-3',
- replayKey: 'cap-3',
- maxPerDay: 2
- });
- assert.equal(capped.recorded, false);
- assert.equal(capped.capped, true);
- const nextDay = recordTrackingEvent({
- sendEventId: eventId,
- eventType: 'open',
- source: 'direct',
- occurredAt: '2026-07-10T00:00:00.000Z',
- userAgent: 'Mozilla/5.0',
- ipHash: 'ip-4',
- replayKey: 'cap-4',
- maxPerDay: 2
- });
- assert.equal(nextDay.recorded, true);
- });
- test('prunes raw tracking events before the retention cutoff', () => {
- initDatabase(tempDataDir(), 'test-secret');
- const user = createUser({ username: 'retention', email: 'retention@example.com', password: 'password123' });
- const domain = createDomain(user.id, domainFixture('retention.example'));
- const eventId = createSendEvent({
- userId: user.id,
- domainId: domain.id,
- sender: 'sender@retention.example',
- recipients: ['reader@example.net'],
- subject: 'Retention',
- trackingOpens: true
- });
- for (const [occurredAt, replayKey] of [
- ['2026-01-01T00:00:00.000Z', 'old-event'],
- ['2026-07-09T00:00:00.000Z', 'new-event']
- ]) {
- recordTrackingEvent({
- sendEventId: eventId,
- eventType: 'open',
- source: 'direct',
- occurredAt,
- userAgent: 'Mozilla/5.0',
- ipHash: replayKey,
- replayKey
- });
- }
- assert.equal(pruneTrackingEvents({ days: 30, now: '2026-07-10T00:00:00.000Z' }), 1);
- const detail = getSendEvent(user.id, eventId);
- assert.equal(detail.tracking.events.length, 1);
- assert.equal(detail.tracking.events[0].occurredAt, '2026-07-09T00:00:00.000Z');
- });
- test('isolates domains, smtp credentials, and api tokens by user', () => {
- initDatabase(tempDataDir(), 'test-secret');
- const admin = seedAdminUser({ username: 'admin', email: 'admin@example.com', password: 'password123' });
- claimLegacyData(admin.id);
- const alice = createUser({ username: 'alice', email: 'alice@example.com', password: 'password123' });
- const bob = createUser({ username: 'bob', email: 'bob@example.com', password: 'password123' });
- assert.equal(authenticateUser('alice', 'password123').id, alice.id);
- assert.equal(authenticateUser('alice', 'wrong'), null);
- createDomain(alice.id, domainFixture('alice.example'));
- assert.equal(listDomains(alice.id).length, 1);
- assert.equal(listDomains(bob.id).length, 0);
- logSendEvent({
- userId: alice.id,
- domainId: listDomains(alice.id)[0].id,
- sender: 'noreply@alice.example',
- recipients: ['user@example.com'],
- subject: 'Hello',
- status: 'queued'
- });
- assert.equal(listSendEvents(alice.id).length, 1);
- assert.equal(listSendEvents(bob.id).length, 0);
- saveSmtpCredential(alice.id, { username: 'smtp-alice', password: 'copy-me-123' });
- assert.equal(getSmtpCredential(alice.id, { includePassword: true }).password, 'copy-me-123');
- assert.equal(verifySmtpCredential('smtp-alice', 'copy-me-123').user.id, alice.id);
- assert.equal(verifySmtpCredential('smtp-alice', 'wrong'), null);
- const token = createApiToken(alice.id, 'send');
- assert.equal(verifyApiToken(token.token).id, alice.id);
- });
- test('stores multiple smtp login credentials per user', () => {
- initDatabase(tempDataDir(), 'test-secret');
- const alice = createUser({ username: 'alice', email: 'alice@example.com', password: 'password123' });
- const bob = createUser({ username: 'bob', email: 'bob@example.com', password: 'password123' });
- const first = saveSmtpCredential(alice.id, { username: 'alice-main', password: 'first-secret' });
- const second = saveSmtpCredential(alice.id, { username: 'alice-app', password: 'second-secret' });
- saveSmtpCredential(bob.id, { username: 'bob-main', password: 'bob-secret' });
- assert.equal(first.passwordSet, true);
- assert.equal(second.passwordSet, true);
- assert.deepEqual(listSmtpCredentials(alice.id).map((credential) => credential.username), ['alice-app', 'alice-main']);
- assert.equal(getSmtpCredential(first.id, alice.id, { includePassword: true }).password, 'first-secret');
- assert.equal(getSmtpCredential(first.id, bob.id), null);
- assert.equal(verifySmtpCredential('alice-main', 'first-secret').user.id, alice.id);
- assert.equal(verifySmtpCredential('alice-app', 'second-secret').user.id, alice.id);
- assert.equal(verifySmtpCredential('alice-app', 'wrong'), null);
- assert.equal(deleteSmtpCredential(first.id, alice.id), true);
- assert.equal(verifySmtpCredential('alice-main', 'first-secret'), null);
- assert.equal(verifySmtpCredential('alice-app', 'second-secret').user.id, alice.id);
- });
- test('stores multiple outbound smtp relays with encrypted recoverable passwords', () => {
- const database = initDatabase(tempDataDir(), 'test-secret');
- const alice = createUser({ username: 'alice', email: 'alice@example.com', password: 'password123' });
- const bob = createUser({ username: 'bob', email: 'bob@example.com', password: 'password123' });
- const primary = saveSmtpRelay(alice.id, {
- name: 'Primary relay',
- host: 'smtp.primary.example',
- port: 587,
- secure: false,
- username: 'primary-user',
- password: 'primary-secret',
- helo: 'mail.alice.example',
- isDefault: true
- });
- const backup = saveSmtpRelay(alice.id, {
- name: 'Backup relay',
- host: 'smtp.backup.example',
- port: 465,
- secure: true,
- username: 'backup-user',
- password: 'backup-secret',
- isDefault: true
- });
- saveSmtpRelay(bob.id, {
- name: 'Bob relay',
- host: 'smtp.bob.example',
- port: 25,
- secure: false,
- username: '',
- password: '',
- isDefault: true
- });
- assert.equal(primary.passwordSet, true);
- assert.equal('password' in primary, false);
- assert.deepEqual(
- listSmtpRelays(alice.id).map((relay) => [relay.name, relay.isDefault, 'password' in relay]),
- [
- ['Backup relay', true, false],
- ['Primary relay', false, false]
- ]
- );
- assert.equal(getSmtpRelay(backup.id, alice.id, { includePassword: true }).password, 'backup-secret');
- assert.equal(getSmtpRelay(backup.id, bob.id), null);
- assert.equal(JSON.stringify(listSmtpRelays(alice.id)).includes('backup-secret'), false);
- const stored = database.prepare('SELECT password_secret FROM smtp_relays WHERE id = ?').get(backup.id);
- assert.ok(stored.password_secret);
- assert.equal(stored.password_secret.includes('backup-secret'), false);
- const updated = saveSmtpRelay(alice.id, {
- id: backup.id,
- name: 'Backup relay updated',
- host: 'smtp.backup.example',
- port: 2525,
- secure: false,
- username: 'backup-user',
- helo: 'helo.backup.example',
- isDefault: false
- });
- assert.equal(updated.port, 2525);
- assert.equal(updated.isDefault, false);
- assert.equal(getSmtpRelay(backup.id, alice.id, { includePassword: true }).password, 'backup-secret');
- const cleared = saveSmtpRelay(alice.id, {
- id: backup.id,
- name: 'Backup relay without auth',
- host: 'smtp.backup.example',
- username: '',
- password: ''
- });
- assert.equal(cleared.passwordSet, false);
- assert.equal(getSmtpRelay(backup.id, alice.id, { includePassword: true }).password, '');
- assert.equal(saveSmtpRelay(bob.id, {
- id: backup.id,
- name: 'Should not create',
- host: 'smtp.invalid.example'
- }), null);
- assert.equal(deleteSmtpRelay(backup.id, bob.id), false);
- assert.equal(deleteSmtpRelay(backup.id, alice.id), true);
- assert.equal(getSmtpRelay(backup.id, alice.id), null);
- });
- test('returns scoped send event detail with webhook deliveries', () => {
- initDatabase(tempDataDir(), 'test-secret');
- const alice = createUser({ username: 'alice', email: 'alice@example.com', password: 'password123' });
- const bob = createUser({ username: 'bob', email: 'bob@example.com', password: 'password123' });
- const domain = createDomain(alice.id, domainFixture('detail.example'));
- createWebhook(alice.id, {
- name: 'Delivery audit',
- url: 'https://hooks.example.com/mailhub',
- events: ['sent']
- });
- const eventId = logSendEvent({
- userId: alice.id,
- domainId: domain.id,
- sender: 'noreply@detail.example',
- recipients: ['user@example.com'],
- subject: 'Tracked message',
- status: 'sent',
- detail: '250 2.0.0 queued as QDETAIL',
- queueId: 'QDETAIL',
- deliveryAttempts: [{
- at: '2026-07-09T12:00:00.000Z',
- queueId: 'QDETAIL',
- recipient: 'user@example.com',
- relay: 'mx.example.net',
- dsn: '2.0.0',
- status: 'sent',
- response: '250 2.0.0 ok'
- }],
- deliveryLog: [{
- at: '2026-07-09T11:59:58.000Z',
- phase: 'queue',
- direction: 'server',
- response: '250 2.0.0 queued as QDETAIL',
- ok: true
- }],
- deliveredAt: '2026-07-09T12:00:00.000Z'
- });
- const detail = getSendEvent(alice.id, eventId);
- assert.equal(detail.id, eventId);
- assert.equal(detail.domain, 'detail.example');
- assert.equal(detail.queueId, 'QDETAIL');
- assert.equal(detail.deliveryAttempts[0].relay, 'mx.example.net');
- assert.equal(detail.deliveryLog[0].phase, 'queue');
- assert.equal(detail.webhookDeliveries.length, 1);
- assert.equal(detail.webhookDeliveries[0].sendEventId, eventId);
- assert.equal(detail.webhookDeliveries[0].eventType, 'sent');
- assert.equal(getSendEvent(bob.id, eventId), null);
- });
- test('send analytics includes delivery funnel and top failure reasons', () => {
- initDatabase(tempDataDir(), 'test-secret');
- const alice = createUser({ username: 'alice', email: 'alice@example.com', password: 'password123' });
- const domain = createDomain(alice.id, domainFixture('analytics.example'));
- for (const [index, status] of ['sent', 'sent', 'queued'].entries()) {
- logSendEvent({
- userId: alice.id,
- domainId: domain.id,
- sender: 'noreply@analytics.example',
- recipients: ['user@example.com'],
- subject: `Message ${status}`,
- status,
- detail: status === 'queued' ? 'queued as QPENDING' : '250 2.0.0 ok',
- queueId: status === 'queued' ? 'QPENDING' : `QSENT${index + 1}`
- });
- }
- logSendEvent({
- userId: alice.id,
- domainId: domain.id,
- sender: 'noreply@analytics.example',
- recipients: ['bad@example.com'],
- subject: 'Bounce',
- status: 'bounced',
- detail: 'Mailbox unavailable',
- queueId: 'QBOUNCE'
- });
- logSendEvent({
- userId: alice.id,
- domainId: domain.id,
- sender: 'noreply@analytics.example',
- recipients: ['bad2@example.com'],
- subject: 'Failure',
- status: 'failed',
- detail: 'Mailbox\t unavailable'
- });
- const analytics = getSendAnalytics(alice.id, { days: 7 });
- assert.equal(analytics.summary.submitted, 5);
- assert.equal(analytics.summary.accepted, 4);
- assert.equal(analytics.summary.delivered, 2);
- assert.equal(analytics.summary.pending, 1);
- assert.equal(analytics.summary.failed, 2);
- assert.equal(analytics.summary.acceptanceRate, 80);
- assert.equal(analytics.summary.deliveryRate, 40);
- assert.equal(analytics.summary.failureRate, 40);
- assert.deepEqual(analytics.deliveryFunnel.map((item) => [item.stage, item.total, item.rate]), [
- ['submitted', 5, 100],
- ['accepted', 4, 80],
- ['delivered', 2, 40],
- ['pending', 1, 20],
- ['failed', 2, 40]
- ]);
- assert.equal(analytics.failureReasons[0].reason, 'Mailbox unavailable');
- assert.equal(analytics.failureReasons[0].total, 2);
- });
- test('send analytics reports delivered engagement rates trends and grouped links', () => {
- initDatabase(tempDataDir(), 'test-secret');
- const user = createUser({ username: 'analytics-track', email: 'analytics-track@example.com', password: 'password123' });
- const domain = createDomain(user.id, domainFixture('analytics-track.example'));
- const now = new Date().toISOString();
- const target = 'https://example.net/reset?token=private';
- const fingerprint = trackingTargetFingerprint(target, 'tracking-secret');
- const events = [];
- for (let index = 0; index < 3; index += 1) {
- events.push(createSendEvent({
- userId: user.id,
- domainId: domain.id,
- sender: 'sender@analytics-track.example',
- recipients: [`reader-${index}@example.net`],
- subject: `Tracked ${index}`,
- status: 'sent',
- trackingOpens: true,
- trackingClicks: true
- }));
- }
- const links = events.slice(0, 2).map((eventId, index) => {
- const token = createTrackingToken();
- const id = createTrackingLink(user.id, eventId, {
- token,
- targetCiphertext: encryptTrackingTarget(target, 'tracking-secret'),
- targetFingerprint: fingerprint,
- targetOrigin: 'https://example.net'
- });
- return { id, token, index };
- });
- const engagement = [
- { sendEventId: events[0], eventType: 'open', source: 'direct', key: 'open-direct' },
- { sendEventId: events[0], eventType: 'open', source: 'proxy', key: 'open-proxy' },
- { sendEventId: events[0], trackingLinkId: links[0].id, eventType: 'click', source: 'direct', key: 'click-one' },
- { sendEventId: events[1], trackingLinkId: links[1].id, eventType: 'click', source: 'scanner', key: 'click-scanner' },
- { sendEventId: events[1], trackingLinkId: links[1].id, eventType: 'click', source: 'direct', key: 'click-two' }
- ];
- for (const item of engagement) {
- recordTrackingEvent({
- ...item,
- occurredAt: now,
- userAgent: item.source === 'scanner' ? 'Scanner' : 'Mozilla/5.0',
- ipHash: item.key,
- replayKey: item.key
- });
- }
- const analytics = getSendAnalytics(user.id, { days: 7, trackingSecret: 'tracking-secret' });
- assert.equal(analytics.engagement.trackedDelivered, 3);
- assert.equal(analytics.engagement.totalOpens, 2);
- assert.equal(analytics.engagement.uniqueOpens, 2);
- assert.equal(analytics.engagement.proxyOpens, 1);
- assert.equal(analytics.engagement.totalClicks, 2);
- assert.equal(analytics.engagement.uniqueClicks, 2);
- assert.equal(analytics.engagement.scannerEvents, 1);
- assert.equal(analytics.engagement.openRate, 66.7);
- assert.equal(analytics.engagement.clickRate, 66.7);
- assert.equal(analytics.engagement.clickToOpenRate, 100);
- assert.equal(analytics.engagementByDay.reduce((sum, day) => sum + day.clicks, 0), 2);
- assert.equal(analytics.topLinks.length, 1);
- assert.equal(analytics.topLinks[0].clicks, 2);
- assert.equal(analytics.topLinks[0].uniqueClicks, 2);
- assert.equal(analytics.topLinks[0].target, target);
- });
- test('send analytics does not truncate windows above 5000 messages', () => {
- const database = initDatabase(tempDataDir(), 'test-secret');
- const user = createUser({ username: 'large-analytics', email: 'large-analytics@example.com', password: 'password123' });
- const createdAt = new Date().toISOString();
- const insert = database.prepare(`
- INSERT INTO send_events (
- user_id, domain_id, sender, recipients, subject, status, detail, created_at
- ) VALUES (?, NULL, ?, '[]', ?, 'queued', '', ?)
- `);
- database.exec('BEGIN');
- for (let index = 0; index < 5001; index += 1) {
- insert.run(user.id, 'sender@example.com', `Message ${index}`, createdAt);
- }
- database.exec('COMMIT');
- assert.equal(getSendAnalytics(user.id, { days: 1 }).summary.total, 5001);
- });
- test('stores account tokens as hashes and enforces token lifecycle', () => {
- const database = initDatabase(tempDataDir(), 'test-secret');
- const alice = createUser({ username: 'alice', email: 'alice@example.com', password: 'password123' });
- const bob = createUser({ username: 'bob', email: 'bob@example.com', password: 'password123' });
- const purpose = 'email_verification';
- const created = createAccountToken(alice.id, purpose, { ttlMinutes: 30 });
- const expectedHash = createHash('sha256').update(created.token).digest('hex');
- assert.equal(created.userId, alice.id);
- assert.equal(created.purpose, purpose);
- assert.equal(typeof created.token, 'string');
- assert.equal(created.token.length > 32, true);
- assert.equal(created.usedAt, null);
- assert.equal('tokenHash' in created, false);
- const stored = database.prepare('SELECT * FROM account_tokens WHERE id = ?').get(created.id);
- assert.equal(stored.user_id, alice.id);
- assert.equal(stored.purpose, purpose);
- assert.equal(stored.token_hash, expectedHash);
- assert.notEqual(stored.token_hash, created.token);
- assert.equal(Object.values(stored).includes(created.token), false);
- assert.equal(consumeAccountToken(created.token, 'password_reset'), null);
- const consumed = consumeAccountToken(created.token, purpose);
- assert.equal(consumed.id, created.id);
- assert.equal(consumed.userId, alice.id);
- assert.equal(consumed.purpose, purpose);
- assert.equal(typeof consumed.usedAt, 'string');
- assert.equal('token' in consumed, false);
- assert.equal(consumeAccountToken(created.token, purpose), null);
- const expired = createAccountToken(alice.id, purpose, { ttlMinutes: 30 });
- database
- .prepare('UPDATE account_tokens SET expires_at = ? WHERE id = ?')
- .run('2000-01-01T00:00:00.000Z', expired.id);
- assert.equal(consumeAccountToken(expired.token, purpose), null);
- const alreadyUsedReset = createAccountToken(alice.id, 'password_reset', { ttlMinutes: 30 });
- assert.equal(consumeAccountToken(alreadyUsedReset.token, 'password_reset').id, alreadyUsedReset.id);
- const reset = createAccountToken(alice.id, 'password_reset', { ttlMinutes: 30 });
- const otherPurpose = createAccountToken(alice.id, 'email_change', { ttlMinutes: 30 });
- const otherUserReset = createAccountToken(bob.id, 'password_reset', { ttlMinutes: 30 });
- assert.equal(invalidateAccountTokens(alice.id, 'password_reset'), 1);
- assert.equal(typeof database.prepare('SELECT used_at FROM account_tokens WHERE id = ?').get(reset.id).used_at, 'string');
- assert.equal(consumeAccountToken(reset.token, 'password_reset'), null);
- assert.equal(consumeAccountToken(alreadyUsedReset.token, 'password_reset'), null);
- assert.equal(consumeAccountToken(otherPurpose.token, 'email_change').id, otherPurpose.id);
- assert.equal(consumeAccountToken(otherUserReset.token, 'password_reset').id, otherUserReset.id);
- });
- test('password updates invalidate unused password reset tokens only', () => {
- initDatabase(tempDataDir(), 'test-secret');
- const alice = createUser({ username: 'alice', email: 'alice@example.com', password: 'password123' });
- const reset = createAccountToken(alice.id, 'password_reset', { ttlMinutes: 30 });
- const verification = createAccountToken(alice.id, 'email_verification', { ttlMinutes: 30 });
- updateUser(alice.id, { password: 'new-password-123' });
- assert.equal(consumeAccountToken(reset.token, 'password_reset'), null);
- assert.equal(consumeAccountToken(verification.token, 'email_verification').id, verification.id);
- assert.equal(authenticateUser('alice', 'password123'), null);
- assert.equal(authenticateUser('alice', 'new-password-123').id, alice.id);
- });
- test('validates account token ttl minute boundaries', () => {
- initDatabase(tempDataDir(), 'test-secret');
- const alice = createUser({ username: 'alice', email: 'alice@example.com', password: 'password123' });
- const maxTtlMinutes = 7 * 24 * 60;
- for (const ttlMinutes of [0, -1, 1.5, maxTtlMinutes + 1, Number.NaN, Number.POSITIVE_INFINITY]) {
- assert.throws(
- () => createAccountToken(alice.id, 'email_verification', { ttlMinutes }),
- /令牌有效期不正确。/
- );
- }
- assert.equal(createAccountToken(alice.id, 'email_verification', { ttlMinutes: 1 }).userId, alice.id);
- assert.equal(createAccountToken(alice.id, 'password_reset', { ttlMinutes: maxTtlMinutes }).userId, alice.id);
- });
- test('creates users with account tokens atomically', () => {
- const database = initDatabase(tempDataDir(), 'test-secret');
- assert.throws(
- () => createUserWithAccountToken({
- username: 'rollback',
- email: 'rollback@example.com',
- password: 'password123',
- status: 'pending_email'
- }, 'email_verification', { ttlMinutes: 0 }),
- /令牌有效期不正确。/
- );
- assert.equal(
- database
- .prepare("SELECT COUNT(*) AS count FROM users WHERE username = 'rollback' OR email = 'rollback@example.com'")
- .get()
- .count,
- 0
- );
- assert.equal(database.prepare('SELECT COUNT(*) AS count FROM account_tokens').get().count, 0);
- const { user, accountToken } = createUserWithAccountToken({
- username: 'atomic',
- email: 'atomic@example.com',
- password: 'password123',
- status: 'pending_email'
- }, 'email_verification', { ttlMinutes: 30 });
- assert.equal(user.status, 'pending_email');
- assert.equal(accountToken.userId, user.id);
- assert.equal(accountToken.purpose, 'email_verification');
- assert.equal(typeof accountToken.token, 'string');
- assert.equal('tokenHash' in accountToken, false);
- assert.equal(
- database
- .prepare('SELECT COUNT(*) AS count FROM account_tokens WHERE user_id = ? AND purpose = ?')
- .get(user.id, 'email_verification')
- .count,
- 1
- );
- });
- test('moves users through the extended status lifecycle', () => {
- initDatabase(tempDataDir(), 'test-secret');
- const pendingEmail = createUser({
- username: 'pending',
- email: 'pending@example.com',
- password: 'password123',
- status: 'pending_email'
- });
- const seededAdmin = seedAdminUser({ username: 'admin', email: 'admin@example.com', password: 'password123' });
- assert.equal(pendingEmail.status, 'pending_email');
- assert.equal(seededAdmin.status, 'active');
- assert.equal(markUserEmailVerified(pendingEmail.id).status, 'pending_review');
- assert.equal(markUserEmailVerified(pendingEmail.id).status, 'pending_review');
- assert.equal(approveUser(pendingEmail.id).status, 'active');
- assert.equal(markUserEmailVerified(pendingEmail.id).status, 'active');
- assert.equal(updateUserStatus(pendingEmail.id, 'disabled').status, 'disabled');
- assert.equal(updateUser(pendingEmail.id, { status: 'pending_review' }).status, 'pending_review');
- assert.equal(markUserEmailVerified(999999), null);
- assert.throws(() => updateUserStatus(pendingEmail.id, 'archived'), /用户状态不正确/);
- assert.throws(
- () => createUser({
- username: 'invalidstatus',
- email: 'invalidstatus@example.com',
- password: 'password123',
- status: 'archived'
- }),
- /用户状态不正确/
- );
- assert.equal(getUser(pendingEmail.id).status, 'pending_review');
- });
- test('lists users with owned resource counts', () => {
- initDatabase(tempDataDir(), 'test-secret');
- const alice = createUser({ username: 'alice', email: 'alice@example.com', password: 'password123' });
- const bob = createUser({ username: 'bob', email: 'bob@example.com', password: 'password123' });
- const aliceDomain = createDomain(alice.id, domainFixture('alice.example'));
- createDomain(alice.id, domainFixture('news.alice.example'));
- const bobDomain = createDomain(bob.id, domainFixture('bob.example'));
- saveDnsCredential(alice.id, {
- name: 'Alice Cloudflare',
- provider: 'cloudflare',
- zoneName: 'alice.example',
- credentials: { apiToken: 'alice-secret-dns-token', zoneId: 'alice-zone' }
- });
- saveDnsCredential(alice.id, {
- name: 'Alice News Cloudflare',
- provider: 'cloudflare',
- zoneName: 'news.alice.example',
- credentials: { apiToken: 'alice-news-secret-dns-token', zoneId: 'alice-news-zone' }
- });
- createApiToken(alice.id, 'primary');
- createApiToken(alice.id, 'secondary');
- createApiToken(bob.id, 'primary');
- saveSmtpCredential(alice.id, { username: 'smtp-alice', password: 'smtp-secret-123' });
- saveSmtpCredential(alice.id, { username: 'smtp-alice-app', password: 'smtp-secret-456' });
- logSendEvent({
- userId: alice.id,
- domainId: aliceDomain.id,
- sender: 'noreply@alice.example',
- recipients: ['one@example.com'],
- subject: 'First',
- status: 'queued'
- });
- logSendEvent({
- userId: alice.id,
- domainId: aliceDomain.id,
- sender: 'noreply@alice.example',
- recipients: ['two@example.com'],
- subject: 'Second',
- status: 'sent'
- });
- logSendEvent({
- userId: bob.id,
- domainId: bobDomain.id,
- sender: 'noreply@bob.example',
- recipients: ['bob@example.com'],
- subject: 'Bob',
- status: 'queued'
- });
- const users = listUsersWithResourceCounts();
- const aliceWithCounts = users.find((user) => user.id === alice.id);
- const bobWithCounts = users.find((user) => user.id === bob.id);
- assert.deepEqual(aliceWithCounts.resourceCounts, {
- domains: 2,
- dnsCredentials: 2,
- apiTokens: 2,
- sendEvents: 2,
- smtpCredential: 2
- });
- assert.deepEqual(bobWithCounts.resourceCounts, {
- domains: 1,
- dnsCredentials: 0,
- apiTokens: 1,
- sendEvents: 1,
- smtpCredential: 0
- });
- assert.equal(typeof aliceWithCounts.resourceCounts.domains, 'number');
- assert.equal('passwordHash' in aliceWithCounts, false);
- assert.equal(JSON.stringify(users).includes('alice-secret-dns-token'), false);
- assert.equal(JSON.stringify(users).includes('smtp-secret-123'), false);
- });
- test('builds admin resource inventory grouped by user with ownership warnings', () => {
- initDatabase(tempDataDir(), 'test-secret');
- const alice = createUser({ username: 'alice', email: 'alice@example.com', password: 'password123' });
- const bob = createUser({ username: 'bob', email: 'bob@example.com', password: 'password123' });
- const bobCredential = saveDnsCredential(bob.id, {
- name: 'Bob DNS',
- provider: 'cloudflare',
- zoneName: 'bob.example',
- credentials: { apiToken: 'bob-secret-token', zoneId: 'bob-zone' }
- });
- const aliceDomain = createDomain(alice.id, {
- ...domainFixture('alice.example'),
- dnsCredentialId: bobCredential.id
- });
- createDomain(bob.id, domainFixture('bob.example'));
- createApiToken(alice.id, 'primary');
- saveSmtpCredential(alice.id, { username: 'smtp-alice', password: 'smtp-secret-123' });
- logSendEvent({
- userId: alice.id,
- domainId: aliceDomain.id,
- sender: 'noreply@alice.example',
- recipients: ['a@example.com'],
- subject: 'Queued',
- status: 'queued'
- });
- const inventory = getAdminResourceInventory();
- const aliceResources = inventory.users.find((entry) => entry.user.id === alice.id);
- const bobResources = inventory.users.find((entry) => entry.user.id === bob.id);
- assert.equal(aliceResources.domains.length, 1);
- assert.equal(aliceResources.domains[0].domain, 'alice.example');
- assert.equal(aliceResources.dnsCredentials.length, 0);
- assert.equal(aliceResources.apiTokens.length, 1);
- assert.equal(aliceResources.smtpCredential.username, 'smtp-alice');
- assert.equal(aliceResources.smtpCredential.passwordSet, true);
- assert.equal(aliceResources.sendEventCount, 1);
- assert.equal(bobResources.domains.length, 1);
- assert.equal(bobResources.dnsCredentials.length, 1);
- assert.deepEqual(inventory.warnings, [{
- type: 'domain_dns_credential_owner_mismatch',
- domainId: aliceDomain.id,
- domain: 'alice.example',
- domainUserId: alice.id,
- dnsCredentialId: bobCredential.id,
- dnsCredentialUserId: bob.id
- }]);
- assert.equal(JSON.stringify(inventory).includes('bob-secret-token'), false);
- assert.equal(JSON.stringify(inventory).includes('smtp-secret-123'), false);
- });
- test('transfers individual resources with audit logs', () => {
- initDatabase(tempDataDir(), 'test-secret');
- const admin = createUser({ username: 'admin2', email: 'admin2@example.com', password: 'password123', role: 'admin' });
- const alice = createUser({ username: 'alice', email: 'alice@example.com', password: 'password123' });
- const bob = createUser({ username: 'bob', email: 'bob@example.com', password: 'password123' });
- const disabled = createUser({ username: 'disabled', email: 'disabled@example.com', password: 'password123', status: 'disabled' });
- const domainOnlyCredential = saveDnsCredential(alice.id, {
- name: 'Alice DNS 1',
- provider: 'cloudflare',
- zoneName: 'alice.example',
- credentials: { apiToken: 'alice-secret-1' }
- });
- const clearCredential = saveDnsCredential(alice.id, {
- name: 'Alice DNS 2',
- provider: 'cloudflare',
- zoneName: 'clear.example',
- credentials: { apiToken: 'alice-secret-2' }
- });
- const withCredential = saveDnsCredential(alice.id, {
- name: 'Alice DNS 3',
- provider: 'cloudflare',
- zoneName: 'with.example',
- credentials: { apiToken: 'alice-secret-3' }
- });
- const standaloneCredential = saveDnsCredential(alice.id, {
- name: 'Alice DNS 4',
- provider: 'cloudflare',
- zoneName: 'standalone.example',
- credentials: { apiToken: 'alice-secret-4' }
- });
- const domainOnly = createDomain(alice.id, { ...domainFixture('domain-only.example'), dnsCredentialId: domainOnlyCredential.id });
- const clearDomain = createDomain(alice.id, { ...domainFixture('clear.example'), dnsCredentialId: clearCredential.id });
- const withDomain = createDomain(alice.id, { ...domainFixture('with.example'), dnsCredentialId: withCredential.id });
- const apiToken = createApiToken(alice.id, 'primary');
- assert.equal(transferDomain({
- actorUserId: admin.id,
- domainId: domainOnly.id,
- targetUserId: bob.id,
- dnsCredentialMode: 'domain_only'
- }).userId, bob.id);
- assert.equal(getDomain(domainOnly.id).dnsCredentialId, domainOnlyCredential.id);
- assert.equal(getDnsCredential(domainOnlyCredential.id, alice.id).id, domainOnlyCredential.id);
- assert.throws(
- () => transferDomain({
- actorUserId: admin.id,
- domainId: domainOnly.id,
- targetUserId: bob.id,
- dnsCredentialMode: 'with_dns_credential'
- }),
- /DNS 凭据归属不一致。/
- );
- assert.equal(getDnsCredential(domainOnlyCredential.id, alice.id).id, domainOnlyCredential.id);
- assert.equal(transferDomain({
- actorUserId: admin.id,
- domainId: clearDomain.id,
- targetUserId: bob.id,
- dnsCredentialMode: 'clear_dns_credential'
- }).dnsCredentialId, null);
- assert.equal(getDnsCredential(clearCredential.id, alice.id).id, clearCredential.id);
- assert.equal(transferDomain({
- actorUserId: admin.id,
- domainId: withDomain.id,
- targetUserId: bob.id,
- dnsCredentialMode: 'with_dns_credential'
- }).userId, bob.id);
- assert.equal(getDnsCredential(withCredential.id, bob.id).id, withCredential.id);
- assert.equal(transferDnsCredential({
- actorUserId: admin.id,
- credentialId: standaloneCredential.id,
- targetUserId: bob.id
- }).userId, bob.id);
- assert.equal(getDnsCredential(standaloneCredential.id, bob.id).id, standaloneCredential.id);
- const transferredTokens = transferApiTokens({
- actorUserId: admin.id,
- tokenIds: [apiToken.id],
- targetUserId: bob.id
- });
- assert.equal(transferredTokens.length, 1);
- assert.equal(verifyApiToken(apiToken.token).id, bob.id);
- assert.throws(
- () => transferDomain({ actorUserId: admin.id, domainId: domainOnly.id, targetUserId: disabled.id }),
- /目标用户不可用。/
- );
- assert.throws(
- () => transferApiTokens({ actorUserId: admin.id, tokenIds: [apiToken.id], targetUserId: 999999 }),
- /目标用户不可用。/
- );
- const actions = listAuditLogs({ actorUserId: admin.id }).map((entry) => entry.action);
- assert.ok(actions.includes('admin.transfer_domain'));
- assert.ok(actions.includes('admin.transfer_dns_credential'));
- assert.ok(actions.includes('admin.transfer_api_tokens'));
- assert.equal(JSON.stringify(listAuditLogs({ actorUserId: admin.id })).includes('alice-secret'), false);
- });
- test('previews and executes user merge with resource counts and multiple smtp credentials', () => {
- initDatabase(tempDataDir(), 'test-secret');
- const admin = createUser({ username: 'admin3', email: 'admin3@example.com', password: 'password123', role: 'admin' });
- const source = createUser({ username: 'source', email: 'source@example.com', password: 'password123' });
- const target = createUser({ username: 'target', email: 'target@example.com', password: 'password123' });
- const credential = saveDnsCredential(source.id, {
- name: 'Source DNS',
- provider: 'cloudflare',
- zoneName: 'source.example',
- credentials: { apiToken: 'source-secret' }
- });
- const domain = createDomain(source.id, { ...domainFixture('source.example'), dnsCredentialId: credential.id });
- const apiToken = createApiToken(source.id, 'primary');
- saveSmtpCredential(source.id, { username: 'smtp-source', password: 'source-secret-123' });
- saveSmtpCredential(source.id, { username: 'smtp-source-app', password: 'source-secret-456' });
- saveSmtpCredential(target.id, { username: 'smtp-target', password: 'target-secret-123' });
- logSendEvent({
- userId: source.id,
- domainId: domain.id,
- sender: 'noreply@source.example',
- recipients: ['a@example.com'],
- subject: 'Queued',
- status: 'queued'
- });
- const preview = previewUserMerge({ sourceUserId: source.id, targetUserId: target.id });
- assert.equal(preview.confirmationText, 'MERGE source INTO target');
- assert.deepEqual(preview.counts, {
- domains: 1,
- dnsCredentials: 1,
- apiTokens: 1,
- sendEvents: 1,
- smtpCredential: 2
- });
- assert.equal(preview.resources.source.domains[0].domain, 'source.example');
- assert.equal(preview.resources.source.dnsCredentials[0].name, 'Source DNS');
- assert.equal(preview.resources.source.apiTokens[0].name, 'primary');
- assert.equal(preview.resources.source.sendEventCount, 1);
- assert.equal(preview.resources.target.domains.length, 0);
- assert.deepEqual(preview.selectedCounts, {
- domains: 1,
- dnsCredentials: 1,
- apiTokens: 1,
- sendEvents: 1,
- smtpCredential: 2
- });
- assert.equal(preview.smtp.conflict, false);
- assert.deepEqual(preview.warnings, []);
- assert.throws(
- () => executeUserMerge({
- actorUserId: admin.id,
- sourceUserId: source.id,
- targetUserId: target.id,
- confirmation: 'wrong'
- }),
- /确认文本不匹配。/
- );
- assert.equal(getDomain(domain.id).userId, source.id);
- const result = executeUserMerge({
- actorUserId: admin.id,
- sourceUserId: source.id,
- targetUserId: target.id,
- confirmation: preview.confirmationText
- });
- assert.deepEqual(result.counts, {
- domains: 1,
- dnsCredentials: 1,
- apiTokens: 1,
- sendEvents: 1,
- smtpCredential: 2
- });
- assert.equal(getDomain(domain.id).userId, target.id);
- assert.equal(getDnsCredential(credential.id, target.id).id, credential.id);
- assert.equal(verifyApiToken(apiToken.token).id, target.id);
- assert.equal(listSendEvents(target.id).length, 1);
- assert.deepEqual(listSmtpCredentials(target.id).map((item) => item.username).sort(), ['smtp-source', 'smtp-source-app', 'smtp-target']);
- assert.deepEqual(listSmtpCredentials(source.id), []);
- assert.equal(getUser(source.id).status, 'disabled');
- const [audit] = listAuditLogs({ action: 'admin.user_merge' });
- assert.equal(audit.targetUserId, target.id);
- assert.equal(audit.summary.sourceUserId, source.id);
- assert.deepEqual(audit.summary.counts, result.counts);
- assert.equal(JSON.stringify(audit).includes('source-secret'), false);
- assert.equal(JSON.stringify(audit).includes('target-secret'), false);
- });
- test('stores system email settings without exposing smtp password publicly', () => {
- const database = initDatabase(tempDataDir(), 'test-secret');
- const saved = saveSystemEmailSettings({
- host: 'smtp.example.com',
- port: 465,
- secure: true,
- username: 'mailer@example.com',
- password: 'smtp-password-123',
- helo: 'mail.example.com',
- fromEmail: 'notify@example.com',
- fromName: 'MailHub Notify',
- testRecipient: 'admin@example.com'
- });
- assert.equal(saved.host, 'smtp.example.com');
- assert.equal(saved.port, 465);
- assert.equal(saved.secure, true);
- assert.equal(saved.passwordSet, true);
- assert.equal('password' in saved, false);
- assert.equal(JSON.stringify(saved).includes('smtp-password-123'), false);
- const storedRows = database.prepare("SELECT key, value FROM app_settings WHERE key LIKE 'systemEmail.%'").all();
- assert.equal(storedRows.some((row) => row.value === 'smtp-password-123'), false);
- const publicSettings = getSystemEmailSettings();
- assert.equal(publicSettings.passwordSet, true);
- assert.equal('password' in publicSettings, false);
- assert.equal(JSON.stringify(publicSettings).includes('smtp-password-123'), false);
- const internalSettings = getSystemEmailSettings({ includeSecret: true });
- assert.equal(internalSettings.password, 'smtp-password-123');
- assert.equal(internalSettings.passwordSet, true);
- const unchangedPassword = saveSystemEmailSettings({
- host: 'smtp2.example.com',
- password: ''
- });
- assert.equal(unchangedPassword.host, 'smtp2.example.com');
- assert.equal(unchangedPassword.passwordSet, true);
- assert.equal(getSystemEmailSettings({ includeSecret: true }).password, 'smtp-password-123');
- });
- test('summarizes send analytics by user', () => {
- initDatabase(tempDataDir(), 'test-secret');
- const alice = createUser({ username: 'alice', email: 'alice@example.com', password: 'password123' });
- const bob = createUser({ username: 'bob', email: 'bob@example.com', password: 'password123' });
- const aliceDomain = createDomain(alice.id, domainFixture('alice.example'));
- const bobDomain = createDomain(bob.id, domainFixture('bob.example'));
- logSendEvent({
- userId: alice.id,
- domainId: aliceDomain.id,
- sender: 'noreply@alice.example',
- recipients: ['a@example.com', 'b@example.com'],
- subject: 'Queued',
- status: 'queued'
- });
- logSendEvent({
- userId: alice.id,
- domainId: aliceDomain.id,
- sender: 'noreply@alice.example',
- recipients: ['c@example.com'],
- subject: 'Failed',
- status: 'failed',
- detail: 'relay rejected'
- });
- logSendEvent({
- userId: bob.id,
- domainId: bobDomain.id,
- sender: 'noreply@bob.example',
- recipients: ['x@example.com'],
- subject: 'Hidden',
- status: 'queued'
- });
- const analytics = getSendAnalytics(alice.id, { days: 7 });
- assert.equal(analytics.summary.total, 2);
- assert.equal(analytics.summary.queued, 1);
- assert.equal(analytics.summary.failed, 1);
- assert.equal(analytics.summary.recipients, 3);
- assert.equal(analytics.summary.successRate, 50);
- assert.equal(analytics.byDomain.length, 1);
- assert.equal(analytics.byDomain[0].domain, 'alice.example');
- assert.equal(analytics.recentFailures.length, 1);
- assert.equal(analytics.recentFailures[0].detail, 'relay rejected');
- });
- test('excludes queued and sent messages from recent delivery failures', () => {
- initDatabase(tempDataDir(), 'test-secret');
- const alice = createUser({ username: 'alice', email: 'alice@example.com', password: 'password123' });
- const domain = createDomain(alice.id, domainFixture('alice.example'));
- for (const event of [
- { subject: 'Queued', status: 'queued', detail: 'accepted by postfix' },
- { subject: 'Sent', status: 'sent', detail: '250 OK' },
- { subject: 'Deferred', status: 'deferred', detail: 'temporary failure' },
- { subject: 'Bounced', status: 'bounced', detail: '550 user unknown' },
- { subject: 'Failed', status: 'failed', detail: 'relay rejected' }
- ]) {
- logSendEvent({
- userId: alice.id,
- domainId: domain.id,
- sender: 'noreply@alice.example',
- recipients: ['user@example.com'],
- subject: event.subject,
- status: event.status,
- detail: event.detail
- });
- }
- const analytics = getSendAnalytics(alice.id, { days: 7 });
- assert.deepEqual(
- analytics.recentFailures.map((event) => event.subject),
- ['Failed', 'Bounced', 'Deferred']
- );
- });
- test('stores and returns structured delivery logs for send events', () => {
- initDatabase(tempDataDir(), 'test-secret');
- const alice = createUser({ username: 'alice', email: 'alice@example.com', password: 'password123' });
- const domain = createDomain(alice.id, domainFixture('alice.example'));
- const deliveryLog = [
- {
- at: '2026-07-08T00:00:00.000Z',
- phase: 'connect',
- direction: 'system',
- message: 'Connected to relay.test:25',
- ok: true
- },
- {
- at: '2026-07-08T00:00:01.000Z',
- phase: 'queue',
- direction: 'server',
- code: 250,
- response: '250 queued as ABC123',
- ok: true
- }
- ];
- logSendEvent({
- userId: alice.id,
- domainId: domain.id,
- sender: 'noreply@alice.example',
- recipients: ['user@example.com'],
- subject: 'Delivery log',
- status: 'queued',
- detail: '250 queued as ABC123',
- deliveryLog
- });
- const [event] = listSendEvents(alice.id);
- assert.deepEqual(event.deliveryLog, deliveryLog);
- });
- test('records audit logs without storing secrets', () => {
- initDatabase(tempDataDir(), 'test-secret');
- const admin = createUser({ username: 'admin2', email: 'admin2@example.com', password: 'password123', role: 'admin' });
- const user = createUser({ username: 'alice', email: 'alice@example.com', password: 'password123' });
- logAudit({
- actorUserId: admin.id,
- action: 'admin.temporary_password',
- targetType: 'user',
- targetId: String(user.id),
- targetUserId: user.id,
- summary: {
- username: user.username,
- temporaryPassword: 'secret-password',
- passwordSet: true,
- dkimPrivate: 'private-key',
- dkimPublic: 'public-key',
- dkim_private: 'private-key',
- dkim_public: 'public-key',
- authorization: 'Bearer top-level-token',
- headers: {
- authorization: 'Bearer nested-token',
- from: 'admin@example.com'
- },
- nested: {
- apiToken: 'secret-token',
- note: 'kept'
- },
- changes: [
- {
- credential: 'secret-credential',
- field: 'password'
- },
- {
- field: 'password',
- value: 'plain-secret',
- label: 'password change'
- },
- {
- name: 'apiToken',
- oldValue: 'old-token',
- newValue: 'new-token'
- },
- {
- header: 'authorization',
- value: 'Bearer token',
- status: 'set'
- },
- {
- path: 'smtp.password',
- from: 'old-password',
- to: 'new-password'
- },
- {
- key: 'credentials.accessKeySecret',
- before: 'old-secret',
- after: 'new-secret'
- },
- {
- field: 'displayName',
- value: 'Alice Example'
- },
- {
- field: 'password',
- old: 'old-secret',
- new: 'new-secret'
- },
- {
- change: { field: 'password' },
- value: 'plain-secret'
- },
- {
- context: { path: 'smtp.password' },
- from: 'old-wrapper-secret',
- to: 'new-wrapper-secret'
- },
- {
- change: { field: 'displayName' },
- value: 'Alice Wrapper'
- }
- ]
- }
- });
- const [entry] = listAuditLogs({ actorUserId: admin.id });
- assert.equal(entry.action, 'admin.temporary_password');
- assert.equal(entry.targetType, 'user');
- assert.equal(entry.targetId, String(user.id));
- assert.equal(entry.targetUserId, user.id);
- assert.equal(entry.summary.username, 'alice');
- assert.equal(entry.summary.temporaryPassword, undefined);
- assert.equal(entry.summary.passwordSet, true);
- assert.equal(entry.summary.dkimPrivate, undefined);
- assert.equal(entry.summary.dkimPublic, 'public-key');
- assert.equal(entry.summary.dkim_private, undefined);
- assert.equal(entry.summary.dkim_public, 'public-key');
- assert.equal(entry.summary.authorization, undefined);
- assert.equal(entry.summary.headers.authorization, undefined);
- assert.equal(entry.summary.headers.from, 'admin@example.com');
- assert.equal(entry.summary.nested.apiToken, undefined);
- assert.equal(entry.summary.nested.note, 'kept');
- assert.equal(entry.summary.changes[0].credential, undefined);
- assert.equal(entry.summary.changes[0].field, 'password');
- assert.equal(entry.summary.changes[1].field, 'password');
- assert.equal(entry.summary.changes[1].value, undefined);
- assert.equal(entry.summary.changes[1].label, 'password change');
- assert.equal(entry.summary.changes[2].name, 'apiToken');
- assert.equal(entry.summary.changes[2].oldValue, undefined);
- assert.equal(entry.summary.changes[2].newValue, undefined);
- assert.equal(entry.summary.changes[3].header, 'authorization');
- assert.equal(entry.summary.changes[3].value, undefined);
- assert.equal(entry.summary.changes[3].status, 'set');
- assert.equal(entry.summary.changes[4].path, 'smtp.password');
- assert.equal(entry.summary.changes[4].from, undefined);
- assert.equal(entry.summary.changes[4].to, undefined);
- assert.equal(entry.summary.changes[5].key, undefined);
- assert.equal(entry.summary.changes[5].before, undefined);
- assert.equal(entry.summary.changes[5].after, undefined);
- assert.equal(entry.summary.changes[6].field, 'displayName');
- assert.equal(entry.summary.changes[6].value, 'Alice Example');
- assert.equal(entry.summary.changes[7].field, 'password');
- assert.equal(entry.summary.changes[7].old, undefined);
- assert.equal(entry.summary.changes[7].new, undefined);
- assert.deepEqual(entry.summary.changes[8].change, { field: 'password' });
- assert.equal(entry.summary.changes[8].value, undefined);
- assert.deepEqual(entry.summary.changes[9].context, { path: 'smtp.password' });
- assert.equal(entry.summary.changes[9].from, undefined);
- assert.equal(entry.summary.changes[9].to, undefined);
- assert.deepEqual(entry.summary.changes[10].change, { field: 'displayName' });
- assert.equal(entry.summary.changes[10].value, 'Alice Wrapper');
- });
- test('filters audit logs and returns newest entries first', () => {
- initDatabase(tempDataDir(), 'test-secret');
- const admin = createUser({ username: 'admin3', email: 'admin3@example.com', password: 'password123', role: 'admin' });
- const alice = createUser({ username: 'alice2', email: 'alice2@example.com', password: 'password123' });
- const bob = createUser({ username: 'bob2', email: 'bob2@example.com', password: 'password123' });
- logAudit({
- actorUserId: admin.id,
- action: 'admin.disable_user',
- targetType: 'user',
- targetId: String(alice.id),
- targetUserId: alice.id,
- summary: { username: alice.username }
- });
- logAudit({
- actorUserId: admin.id,
- action: 'admin.reset_password',
- targetType: 'user',
- targetId: String(alice.id),
- targetUserId: alice.id,
- summary: { username: alice.username }
- });
- logAudit({
- actorUserId: null,
- action: 'system.rotation',
- targetType: 'system',
- summary: { reason: 'scheduled' }
- });
- logAudit({
- actorUserId: admin.id,
- action: 'admin.reset_password',
- targetType: 'user',
- targetId: String(bob.id),
- targetUserId: bob.id,
- summary: { username: bob.username }
- });
- assert.deepEqual(
- listAuditLogs({ action: 'admin.reset_password' }).map((entry) => entry.summary.username),
- ['bob2', 'alice2']
- );
- assert.deepEqual(
- listAuditLogs({ targetUserId: alice.id }).map((entry) => entry.action),
- ['admin.reset_password', 'admin.disable_user']
- );
- assert.deepEqual(
- listAuditLogs({ actorUserId: null }).map((entry) => entry.action),
- ['system.rotation']
- );
- const pastIso = '2000-01-01T00:00:00.000Z';
- const futureIso = '2999-01-01T00:00:00.000Z';
- assert.deepEqual(listAuditLogs({ from: futureIso }), []);
- assert.deepEqual(listAuditLogs({ to: pastIso }), []);
- assert.deepEqual(
- listAuditLogs({ from: pastIso, to: futureIso }).map((entry) => entry.action),
- ['admin.reset_password', 'system.rotation', 'admin.reset_password', 'admin.disable_user']
- );
- });
- function domainFixture(domain) {
- return {
- domain,
- selector: 'mh202607',
- verificationToken: 'token',
- dkimPublic: 'public',
- dkimPrivate: 'private',
- senderHost: `mail.${domain}`,
- sendingIp: '127.0.0.1',
- spfExtra: '',
- dmarcPolicy: 'none',
- dmarcRua: ''
- };
- }
- function tempDataDir() {
- return mkdtempSync(path.join(tmpdir(), 'mailhub-test-'));
- }
|