| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353 |
- 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-import@example.net>',
- '',
- '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);
- }
|