import assert from 'node:assert/strict'; import { existsSync, mkdirSync, mkdtempSync, readFileSync, utimesSync, writeFileSync } from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { DatabaseSync } from 'node:sqlite'; import { test } from 'node:test'; import { createAtomicCheckpointWriter, parseVestaImportArguments, preflightVestaImport, runVestaMaildirImport } from '../scripts/import-vesta-maildir.js'; import { getDomainByName, initDatabase, listInboundMailboxes, listInboundMessages, seedAdminUser } from '../src/db.js'; const legacyHash = '{MD5}$1$hfT7jp2q$G3yf0NUx7mUkX.LIFWQxN.'; test('CLI arguments support production import paths and dry-run', () => { const parsed = parseVestaImportArguments([ '--snapshot', 'snapshot', '--data-dir=data', '--user', 'admin', '--source', 'VESTA:Primary', '--checkpoint', 'state/checkpoint.json', '--dry-run' ], { cwd: '/tmp/mailhub-cli', env: {} }); assert.equal(parsed.snapshot, '/tmp/mailhub-cli/snapshot'); assert.equal(parsed.dataDir, '/tmp/mailhub-cli/data'); assert.equal(parsed.user, 'admin'); assert.equal(parsed.source, 'vesta:primary'); assert.equal(parsed.checkpoint, '/tmp/mailhub-cli/state/checkpoint.json'); assert.equal(parsed.dryRun, true); }); test('dry-run checks all 35 hashes without business writes, then real import resumes safely', async () => { const fixture = createSnapshotFixture(35); const dataDir = mkdtempSync(path.join(os.tmpdir(), 'mailhub-vesta-cli-db-')); initDatabase(dataDir, 'cli-test-secret'); const user = seedAdminUser({ username: 'admin', email: 'admin@example.test', password: 'admin-password' }); const checkpoint = path.join(dataDir, 'import.checkpoint.json'); const output = captureOutput(); const before = businessCounts(dataDir); const common = { snapshot: fixture.root, dataDir, user: 'admin', source: 'vesta:test', checkpoint }; const dryRun = await runVestaMaildirImport({ ...common, dryRun: true }, { env: { MAIL_HOSTNAME: 'smtp.target.test', SENDING_IP: '192.0.2.44' }, output }); assert.deepEqual(businessCounts(dataDir), before); assert.equal(dryRun.preflight.hashes, 35); assert.equal(dryRun.preflight.supportedHashes, 35); assert.equal(dryRun.preflight.unsupportedHashes, 0); assert.equal(dryRun.summary.plannedMessages, 1); assert.equal(dryRun.summary.importedMessages, 0); assert.equal(existsSync(checkpoint), false); assert.match(output.text(), /preflight .*hashes=35 .*supported_hashes=35/); assert.match(output.text(), /complete dry_run=1/); assertNoSensitiveOutput(output.text()); output.clear(); const imported = await runVestaMaildirImport(common, { env: { SESSION_SECRET: 'cli-test-secret', MAIL_HOSTNAME: 'smtp.target.test', SENDING_IP: '192.0.2.44', DEFAULT_SPF_MECHANISMS: 'ip4:192.0.2.44', DMARC_POLICY: 'quarantine', DMARC_RUA: 'mailto:dmarc@target.test' }, output }); assert.equal(imported.summary.importedMessages, 1); assert.equal(listInboundMailboxes(user.id).length, 35); assert.equal(listInboundMessages(user.id, { folder: null }).length, 1); const domain = getDomainByName('import.example', { userId: user.id }); assert.equal(domain.catchAllAddress, 'acct00@import.example'); assert.equal(domain.senderHost, 'smtp.target.test'); assert.equal(domain.sendingIp, '192.0.2.44'); assert.equal(domain.spfExtra, 'ip4:192.0.2.44'); assert.equal(domain.dmarcPolicy, 'quarantine'); assert.equal(domain.dmarcRua, 'mailto:dmarc@target.test'); assertNoSensitiveOutput(output.text()); const checkpointValue = JSON.parse(readFileSync(checkpoint, 'utf8')); assert.equal(checkpointValue.complete, true); assert.equal(checkpointValue.processed, 1); assert.equal(typeof checkpointValue.lastSourceKey, 'string'); assert.equal('warnings' in checkpointValue, false); assert.equal(checkpointValue.warningCount, 34); assertNoSensitiveOutput(JSON.stringify(checkpointValue)); output.clear(); const resumed = await runVestaMaildirImport(common, { env: { SESSION_SECRET: 'cli-test-secret' }, output }); assert.equal(resumed.summary.importedMessages, 0); assert.equal(resumed.summary.skippedMessages, 1); assert.equal(resumed.summary.resumeSkippedMessages, 0); assert.equal(listInboundMessages(user.id, { folder: null }).length, 1); assertNoSensitiveOutput(output.text()); writeFileSync( path.join(fixture.root, 'usr/local/vesta/data/users/admin/mail.conf'), "DOMAIN='import.example' CATCHALL='acct01' SUSPENDED='no'\n" ); await assert.rejects( runVestaMaildirImport(common, { env: { SESSION_SECRET: 'cli-test-secret' }, output }), (error) => error?.category === 'checkpoint' ); assert.equal(getDomainByName('import.example', { userId: user.id }).catchAllAddress, 'acct00@import.example'); }); test('preflight counts unsupported hashes and cross-user conflicts without exposing resource values', () => { const result = preflightVestaImport({ domains: [{ domain: 'conflict.example' }], mailboxes: [{ address: 'private@conflict.example', passwordHash: '{SHA512-CRYPT}redacted' }], warnings: ['sensitive warning'] }, { user: { id: 7 }, domains: [{ domain: 'conflict.example', userId: 8 }], mailboxes: [{ address: 'private@conflict.example', userId: 8 }] }); assert.equal(result.conflicts, 2); assert.equal(result.hashes, 1); assert.equal(result.unsupportedHashes, 1); assert.equal(result.warningCount, 1); assert.equal('warnings' in result, false); }); test('preflight blocks missing hashes and invalid defaults for new domains', () => { const result = preflightVestaImport({ domains: [{ domain: 'new.example' }], mailboxes: [{ address: 'missing@new.example', passwordHash: '' }], warnings: [] }, { user: { id: 7 }, domains: [], mailboxes: [] }, { defaults: { senderHost: 'invalid host', sendingIp: '' } }); assert.equal(result.newDomains, 1); assert.equal(result.missingHashes, 1); assert.equal(result.invalidDefaults, 1); }); test('preflight blocks a soft-deleted mailbox that the import cannot reuse', () => { const result = preflightVestaImport({ domains: [{ domain: 'existing.example' }], mailboxes: [{ address: 'archived@existing.example', passwordHash: legacyHash }], warnings: [] }, { user: { id: 7 }, domains: [{ domain: 'existing.example', userId: 7 }], mailboxes: [{ address: 'archived@existing.example', userId: 7, deletedAt: '2026-07-15T00:00:00.000Z' }] }); assert.equal(result.conflicts, 1); }); test('preflight only allows an existing same-owner mailbox during checkpoint resume', () => { const snapshot = { domains: [{ domain: 'existing.example' }], mailboxes: [{ address: 'owner@existing.example', passwordHash: legacyHash }], warnings: [] }; const inventory = { user: { id: 7 }, domains: [{ domain: 'existing.example', userId: 7 }], mailboxes: [{ address: 'owner@existing.example', userId: 7, deletedAt: null }] }; const firstRun = preflightVestaImport(snapshot, inventory); const resume = preflightVestaImport(snapshot, inventory, { allowExistingMailboxes: true }); assert.equal(firstRun.existingMailboxes, 1); assert.equal(firstRun.conflicts, 1); assert.equal(resume.existingMailboxes, 1); assert.equal(resume.conflicts, 0); }); test('real import writes an incomplete checkpoint before entering the importer', async () => { const directory = mkdtempSync(path.join(os.tmpdir(), 'mailhub-vesta-initial-checkpoint-')); const checkpoint = path.join(directory, 'checkpoint.json'); const user = { id: 7, username: 'admin', email: 'admin@example.test', status: 'active' }; const snapshot = { domains: [{ domain: 'new.example' }], mailboxes: [{ address: 'owner@new.example', passwordHash: legacyHash }], warnings: ['count only'] }; await assert.rejects( runVestaMaildirImport({ snapshot: path.join(directory, 'snapshot'), dataDir: directory, user: 'admin', source: 'vesta:checkpoint', checkpoint }, { env: { MAIL_HOSTNAME: 'smtp.target.test', SENDING_IP: '192.0.2.44' }, inspectTarget: () => ({ user, domains: [], mailboxes: [] }), readSnapshot: async () => snapshot, dbApi: { initDatabase() {}, getUserByLogin: () => user }, importSnapshot: async () => { const initial = JSON.parse(readFileSync(checkpoint, 'utf8')); assert.equal(initial.complete, false); assert.equal(initial.lastSourceKey, ''); assert.equal(initial.processed, 0); assert.equal(initial.warningCount, 1); throw new Error('simulated mailbox-stage crash'); }, output: captureOutput() }), /simulated mailbox-stage crash/ ); assert.equal(JSON.parse(readFileSync(checkpoint, 'utf8')).complete, false); }); test('checkpoint writer atomically flushes every 100 messages or one second', async () => { const directory = mkdtempSync(path.join(os.tmpdir(), 'mailhub-vesta-checkpoint-')); const filePath = path.join(directory, 'checkpoint.json'); let timestamp = 0; const writer = createAtomicCheckpointWriter({ filePath, identity: { version: 1, source: 'vesta:test', targetUserId: 1, snapshotId: 'snapshot-id' }, clock: () => timestamp }); for (let index = 1; index < 100; index += 1) { assert.equal(await writer.record(`source-${index}`, { processed: index, bytes: index }), false); } assert.equal(existsSync(filePath), false); assert.equal(await writer.record('source-100', { processed: 100, bytes: 100 }), true); assert.equal(JSON.parse(readFileSync(filePath, 'utf8')).processed, 100); timestamp = 1000; assert.equal(await writer.record('source-101', { processed: 101, bytes: 101 }), true); const checkpoint = JSON.parse(readFileSync(filePath, 'utf8')); assert.equal(checkpoint.processed, 101); assert.equal(checkpoint.lastSourceKey, 'source-101'); assert.equal(checkpoint.complete, false); }); function createSnapshotFixture(accountCount) { const root = mkdtempSync(path.join(os.tmpdir(), 'mailhub-vesta-cli-snapshot-')); const userData = path.join(root, 'usr/local/vesta/data/users/admin'); const accountConfig = path.join(userData, 'mail'); const mailRoot = path.join(root, 'home/admin/mail/import.example'); const firstMailbox = path.join(mailRoot, 'acct00'); for (const directory of [ accountConfig, path.join(firstMailbox, 'cur'), path.join(firstMailbox, 'new'), path.join(firstMailbox, 'tmp') ]) mkdirSync(directory, { recursive: true }); writeFileSync( path.join(userData, 'mail.conf'), "DOMAIN='import.example' CATCHALL='acct00' SUSPENDED='no'\n" ); const accounts = []; for (let index = 0; index < accountCount; index += 1) { const account = `acct${String(index).padStart(2, '0')}`; accounts.push( `ACCOUNT='${account}' MD5='${legacyHash}' QUOTA='unlimited' SUSPENDED='no' DATE='2024-01-02' TIME='03:04:05'` ); } writeFileSync(path.join(accountConfig, 'import.example.conf'), `${accounts.join('\n')}\n`); const messagePath = path.join(firstMailbox, 'cur', '1700000000.M1P1.host:2,S'); writeFileSync(messagePath, [ 'From: sender@example.net', 'To: acct00@import.example', 'Subject: Private migration subject', 'Message-ID: ', '', 'Private migration body' ].join('\r\n')); const receivedAt = new Date('2024-01-02T03:04:05.000Z'); utimesSync(messagePath, receivedAt, receivedAt); return { root }; } function businessCounts(dataDir) { const database = new DatabaseSync(path.join(dataDir, 'mailhub.sqlite'), { readOnly: true }); try { return { domains: Number(database.prepare('SELECT COUNT(*) AS count FROM domains').get().count), mailboxes: Number(database.prepare('SELECT COUNT(*) AS count FROM inbound_mailboxes').get().count), messages: Number(database.prepare('SELECT COUNT(*) AS count FROM inbound_messages').get().count) }; } finally { database.close(); } } function captureOutput() { let value = ''; return { write(chunk) { value += String(chunk); return true; }, text() { return value; }, clear() { value = ''; } }; } function assertNoSensitiveOutput(value) { assert.doesNotMatch(value, /acct00@import\.example/i); assert.doesNotMatch(value, /\$1\$hfT7jp2q/i); assert.doesNotMatch(value, /Private migration (?:subject|body)/i); }