| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729 |
- #!/usr/bin/env node
- import crypto from 'node:crypto';
- import { existsSync, readFileSync } from 'node:fs';
- import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
- import path from 'node:path';
- import { pathToFileURL } from 'node:url';
- import { DatabaseSync } from 'node:sqlite';
- import {
- createDomain,
- createInboundFolder,
- createImportedInboundMessage,
- getDomainByName,
- getUser,
- getUserByLogin,
- hasImportedInboundMessage,
- initDatabase,
- updateDomain,
- upsertImportedInboundMailbox
- } from '../src/db.js';
- import { createDkimKeyPair } from '../src/dkim.js';
- import { isLegacyPasswordHash } from '../src/password-hash.js';
- import {
- importVestaSnapshot,
- readVestaSnapshotMetadata
- } from '../src/vesta-maildir-import.js';
- const checkpointVersion = 1;
- const checkpointMessageInterval = 100;
- const checkpointTimeIntervalMs = 1000;
- const defaultDbApi = {
- createDomain,
- createInboundFolder,
- createImportedInboundMessage,
- getDomainByName,
- getUser,
- getUserByLogin,
- hasImportedInboundMessage,
- initDatabase,
- updateDomain,
- upsertImportedInboundMailbox
- };
- export class VestaImportCliError extends Error {
- constructor(category, message) {
- super(message);
- this.name = 'VestaImportCliError';
- this.category = category;
- }
- }
- export function parseVestaImportArguments(argv, {
- env = process.env,
- cwd = process.cwd()
- } = {}) {
- const values = {};
- let help = false;
- for (let index = 0; index < argv.length; index += 1) {
- const argument = String(argv[index] || '');
- if (argument === '--help' || argument === '-h') {
- help = true;
- continue;
- }
- if (argument === '--dry-run') {
- values.dryRun = true;
- continue;
- }
- const inline = argument.match(/^--([a-z-]+)=(.*)$/);
- const name = inline?.[1] || argument.match(/^--([a-z-]+)$/)?.[1] || '';
- if (!['snapshot', 'data-dir', 'user', 'source', 'checkpoint'].includes(name)) {
- throw new VestaImportCliError('usage', '导入参数不正确。');
- }
- const value = inline ? inline[2] : argv[++index];
- if (value === undefined || String(value).trim() === '') {
- throw new VestaImportCliError('usage', '导入参数缺少值。');
- }
- values[toCamelCase(name)] = String(value).trim();
- }
- if (help) return { help: true };
- const snapshot = values.snapshot ? path.resolve(cwd, values.snapshot) : '';
- const dataDir = path.resolve(cwd, values.dataDir || env.DATA_DIR || 'data');
- const user = String(values.user || '').trim();
- const source = normalizeImportSource(values.source);
- const checkpoint = path.resolve(
- cwd,
- values.checkpoint || path.join(dataDir, 'vesta-maildir-import.checkpoint.json')
- );
- if (!snapshot || !user || !source) {
- throw new VestaImportCliError('usage', '必须指定快照、目标用户和导入来源。');
- }
- return {
- help: false,
- snapshot,
- dataDir,
- user,
- source,
- checkpoint,
- dryRun: Boolean(values.dryRun)
- };
- }
- export function loadMailHubEnvironment({
- env = process.env,
- cwd = process.cwd()
- } = {}) {
- const file = path.join(cwd, '.env');
- if (!existsSync(file)) return env;
- for (const line of readFileSync(file, 'utf8').split(/\r?\n/)) {
- const clean = line.trim();
- if (!clean || clean.startsWith('#')) continue;
- const separator = clean.indexOf('=');
- if (separator === -1) continue;
- const key = clean.slice(0, separator).trim();
- let value = clean.slice(separator + 1).trim();
- if (!key || key in env) continue;
- if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
- value = value.slice(1, -1);
- }
- env[key] = value;
- }
- return env;
- }
- export async function runVestaMaildirImport(options, {
- env = process.env,
- output = process.stdout,
- clock = Date.now,
- dbApi = defaultDbApi,
- inspectTarget = inspectMailHubTarget,
- readSnapshot = readVestaSnapshotMetadata,
- importSnapshot = importVestaSnapshot,
- hashSupported = isLegacyPasswordHash,
- signal = null
- } = {}) {
- const normalized = normalizeRunOptions(options);
- const inventory = inspectTarget({ dataDir: normalized.dataDir, user: normalized.user });
- const snapshot = await readSnapshot({ root: normalized.snapshot });
- const defaults = domainDefaults(env);
- const checkpointIdentity = {
- version: checkpointVersion,
- source: normalized.source,
- targetUserId: inventory.user.id,
- snapshotId: snapshotIdentifier(normalized.snapshot, snapshot)
- };
- const previousCheckpoint = normalized.dryRun
- ? null
- : await readImportCheckpoint(normalized.checkpoint, checkpointIdentity);
- const preflight = preflightVestaImport(snapshot, inventory, {
- hashSupported,
- defaults,
- allowExistingMailboxes: Boolean(previousCheckpoint)
- });
- writeCountLine(output, 'preflight', {
- domains: preflight.domains,
- new_domains: preflight.newDomains,
- mailboxes: preflight.mailboxes,
- existing_mailboxes: preflight.existingMailboxes,
- hashes: preflight.hashes,
- supported_hashes: preflight.supportedHashes,
- unsupported_hashes: preflight.unsupportedHashes,
- missing_hashes: preflight.missingHashes,
- conflicts: preflight.conflicts,
- invalid_defaults: preflight.invalidDefaults,
- warnings: preflight.warningCount
- });
- if (preflight.conflicts || preflight.unsupportedHashes || preflight.missingHashes || preflight.invalidDefaults) {
- throw new VestaImportCliError('preflight', 'Vesta 导入预检失败。');
- }
- if (normalized.dryRun) {
- const report = await importSnapshot({
- root: normalized.snapshot,
- dryRun: true,
- signal
- });
- const summary = sanitizeImportReport(report);
- writeCompletionLine(output, summary);
- return { preflight, summary, checkpoint: null };
- }
- dbApi.initDatabase(normalized.dataDir, mailHubSecret(env));
- const targetUser = resolveDbUser(dbApi, normalized.user);
- if (!targetUser || targetUser.id !== inventory.user.id || targetUser.status === 'disabled') {
- throw new VestaImportCliError('target', '目标用户不可用。');
- }
- // Always rescan the snapshot and let the database source key deduplicate it.
- // A file checkpoint cannot prove that earlier rows survived a DB rollback.
- const resumeCheckpoint = previousCheckpoint
- ? {
- ...previousCheckpoint,
- lastSourceKey: '',
- processed: 0,
- bytes: 0,
- complete: false
- }
- : null;
- const progress = {
- imported: 0,
- skipped: 0,
- processed: Number(resumeCheckpoint?.processed || 0),
- bytes: Number(resumeCheckpoint?.bytes || 0)
- };
- const checkpointWriter = createAtomicCheckpointWriter({
- filePath: normalized.checkpoint,
- identity: checkpointIdentity,
- initial: {
- ...(resumeCheckpoint || {}),
- warningCount: preflight.warningCount
- },
- clock
- });
- await checkpointWriter.flush({ force: true, complete: false });
- const adapter = createMailHubImportAdapter({
- dbApi,
- user: targetUser,
- source: normalized.source,
- defaults,
- progress
- });
- let report;
- try {
- report = await importSnapshot({
- root: normalized.snapshot,
- adapter,
- resumeAfterSourceKey: '',
- signal,
- onCheckpoint: async (sourceKey, context) => {
- progress.processed += 1;
- progress.bytes += Number(context?.message?.size || context?.message?.rawMessageBytes?.length || 0);
- const flushed = await checkpointWriter.record(sourceKey, progress);
- if (flushed) {
- writeCountLine(output, 'progress', {
- processed: progress.processed,
- imported: progress.imported,
- skipped: progress.skipped,
- bytes: progress.bytes
- });
- }
- }
- });
- } catch (error) {
- await checkpointWriter.flush({ force: true, complete: false });
- throw error;
- }
- const summary = sanitizeImportReport(report);
- await checkpointWriter.flush({
- force: true,
- complete: true,
- report: summary,
- fallbackSourceKey: report.lastSourceKey || resumeCheckpoint?.lastSourceKey || ''
- });
- writeCompletionLine(output, summary);
- return {
- preflight,
- summary,
- checkpoint: checkpointWriter.snapshot()
- };
- }
- export function inspectMailHubTarget({ dataDir, user }) {
- const databaseFile = path.join(path.resolve(dataDir), 'mailhub.sqlite');
- if (!existsSync(databaseFile)) throw new VestaImportCliError('target', '目标数据库不存在。');
- let database;
- try {
- database = new DatabaseSync(databaseFile, { readOnly: true });
- const target = parseTargetUser(user);
- const userRow = target.id
- ? database.prepare('SELECT id, username, email, status FROM users WHERE id = ?').get(target.id)
- : database.prepare('SELECT id, username, email, status FROM users WHERE lower(username) = ? OR lower(email) = ?').get(target.login, target.login);
- if (!userRow || userRow.status === 'disabled') throw new VestaImportCliError('target', '目标用户不可用。');
- const domains = database
- .prepare('SELECT id, user_id, lower(domain) AS domain FROM domains')
- .all()
- .map((row) => ({ id: Number(row.id), userId: Number(row.user_id), domain: row.domain }));
- const mailboxes = database
- .prepare(`
- SELECT
- m.id,
- m.user_id,
- lower(m.address) AS address,
- lower(d.domain) AS domain,
- m.deleted_at
- FROM inbound_mailboxes m
- JOIN domains d ON d.id = m.domain_id
- `)
- .all()
- .map((row) => ({
- id: Number(row.id),
- userId: Number(row.user_id),
- address: row.address,
- domain: row.domain,
- deletedAt: row.deleted_at || null
- }));
- return {
- user: {
- id: Number(userRow.id),
- username: userRow.username,
- email: userRow.email,
- status: userRow.status
- },
- domains,
- mailboxes
- };
- } catch (error) {
- if (error instanceof VestaImportCliError) throw error;
- throw new VestaImportCliError('target', '目标数据库无法读取。');
- } finally {
- database?.close();
- }
- }
- export function preflightVestaImport(snapshot, inventory, {
- hashSupported = isLegacyPasswordHash,
- defaults = {},
- allowExistingMailboxes = false
- } = {}) {
- const targetUserId = Number(inventory.user.id);
- const existingDomains = new Map(inventory.domains.map((domain) => [domain.domain, domain]));
- const existingMailboxes = new Map(inventory.mailboxes.map((mailbox) => [mailbox.address, mailbox]));
- const sourceDomains = new Set();
- const sourceMailboxes = new Set();
- let conflicts = 0;
- let newDomains = 0;
- let hashes = 0;
- let supportedHashes = 0;
- let unsupportedHashes = 0;
- let missingHashes = 0;
- let existingMailboxCount = 0;
- for (const domain of snapshot.domains) {
- const name = normalizeDomainName(domain.domain || domain.name);
- if (!name || sourceDomains.has(name)) conflicts += 1;
- else sourceDomains.add(name);
- const existing = existingDomains.get(name);
- if (existing && existing.userId !== targetUserId) conflicts += 1;
- if (!existing) newDomains += 1;
- }
- for (const mailbox of snapshot.mailboxes) {
- const address = normalizeMailboxAddress(mailbox.address);
- if (!address) {
- conflicts += 1;
- } else {
- if (sourceMailboxes.has(address)) conflicts += 1;
- else sourceMailboxes.add(address);
- const mailboxDomain = address.slice(address.lastIndexOf('@') + 1);
- if (!sourceDomains.has(mailboxDomain)) conflicts += 1;
- const existing = existingMailboxes.get(address);
- if (existing) {
- existingMailboxCount += 1;
- if (
- existing.userId !== targetUserId
- || existing.deletedAt
- || !allowExistingMailboxes
- ) conflicts += 1;
- }
- }
- const passwordHash = String(mailbox.passwordHash || mailbox.legacyPasswordHash || '').trim();
- if (!passwordHash) {
- missingHashes += 1;
- continue;
- }
- hashes += 1;
- if (hashSupported(passwordHash)) supportedHashes += 1;
- else unsupportedHashes += 1;
- }
- return {
- domains: snapshot.domains.length,
- newDomains,
- mailboxes: snapshot.mailboxes.length,
- existingMailboxes: existingMailboxCount,
- hashes,
- supportedHashes,
- unsupportedHashes,
- missingHashes,
- conflicts,
- invalidDefaults: newDomains > 0 && !validNewDomainDefaults(defaults) ? 1 : 0,
- warningCount: Array.isArray(snapshot.warnings) ? snapshot.warnings.length : 0
- };
- }
- export function createMailHubImportAdapter({ dbApi, user, source, defaults, progress }) {
- return {
- async ensureDomain(sourceDomain) {
- const domainName = normalizeDomainName(sourceDomain.domain || sourceDomain.name);
- const catchAllAddress = String(sourceDomain.catchAllAddress ?? sourceDomain.catchAll ?? '').trim();
- const existing = dbApi.getDomainByName(domainName);
- if (existing) {
- if (Number(existing.userId) !== Number(user.id)) {
- throw new VestaImportCliError('conflict', '现有域名归属冲突。');
- }
- return dbApi.updateDomain(existing.id, user.id, { catchAllAddress });
- }
- if (!validNewDomainDefaults(defaults)) {
- throw new VestaImportCliError('configuration', '新域名默认配置不可用。');
- }
- const keys = createDkimKeyPair();
- const created = dbApi.createDomain(user.id, {
- domain: domainName,
- selector: defaultSelector(),
- verificationToken: crypto.randomBytes(18).toString('hex'),
- dkimPublic: keys.publicKey,
- dkimPrivate: keys.privateKey,
- senderHost: defaults.senderHost,
- sendingIp: defaults.sendingIp,
- spfExtra: defaults.spfExtra,
- dmarcPolicy: defaults.dmarcPolicy,
- dmarcRua: defaults.dmarcRua
- });
- return dbApi.updateDomain(created.id, user.id, { catchAllAddress });
- },
- async ensureMailbox(mailbox) {
- return dbApi.upsertImportedInboundMailbox(user.id, {
- address: mailbox.address,
- displayName: mailbox.displayName,
- passwordHash: mailbox.passwordHash || mailbox.legacyPasswordHash || '',
- aliases: mailbox.aliases,
- forwardTo: mailbox.forwardTo,
- keepForwarded: mailbox.keepForwarded ?? !mailbox.forwardOnly,
- quotaMb: mailbox.quotaMb,
- status: mailbox.suspended || mailbox.status === 'suspended' ? 'disabled' : 'active'
- });
- },
- async ensureFolder(mailbox, folder) {
- return dbApi.createInboundFolder(mailbox, folder);
- },
- async hasMessage(sourceKey) {
- const exists = dbApi.hasImportedInboundMessage(source, sourceKey);
- if (exists) progress.skipped += 1;
- return exists;
- },
- async createMessage(message, context) {
- const result = dbApi.createImportedInboundMessage(context.mailbox, {
- ...message,
- importSource: source,
- sourceKey: message.sourceKey
- });
- if (result.created) progress.imported += 1;
- else progress.skipped += 1;
- return result;
- }
- };
- }
- export function createAtomicCheckpointWriter({
- filePath,
- identity,
- initial = null,
- clock = Date.now,
- messageInterval = checkpointMessageInterval,
- timeIntervalMs = checkpointTimeIntervalMs
- }) {
- let pending = 0;
- let lastFlushAt = clock();
- let state = {
- ...identity,
- lastSourceKey: String(initial?.lastSourceKey || ''),
- processed: Number(initial?.processed || 0),
- bytes: Number(initial?.bytes || 0),
- complete: Boolean(initial?.complete),
- warningCount: Number(initial?.warningCount || 0)
- };
- async function flush({ force = false, complete = state.complete, report = null, fallbackSourceKey = '' } = {}) {
- if (!force && pending < messageInterval && clock() - lastFlushAt < timeIntervalMs) return false;
- if (!pending && !force) return false;
- state = {
- ...state,
- lastSourceKey: state.lastSourceKey || String(fallbackSourceKey || ''),
- complete: Boolean(complete),
- warningCount: Number(report?.warningCount ?? state.warningCount ?? 0),
- updatedAt: new Date(clock()).toISOString()
- };
- await atomicWriteJson(filePath, state);
- pending = 0;
- lastFlushAt = clock();
- return true;
- }
- return {
- async record(sourceKey, progress) {
- state = {
- ...state,
- lastSourceKey: String(sourceKey || state.lastSourceKey || ''),
- processed: Number(progress.processed || 0),
- bytes: Number(progress.bytes || 0),
- complete: false
- };
- pending += 1;
- if (pending < messageInterval && clock() - lastFlushAt < timeIntervalMs) return false;
- return await flush();
- },
- flush,
- snapshot() {
- return { ...state };
- }
- };
- }
- export async function readImportCheckpoint(filePath, identity) {
- let checkpoint;
- try {
- checkpoint = JSON.parse(await readFile(filePath, 'utf8'));
- } catch (error) {
- if (error?.code === 'ENOENT') return null;
- throw new VestaImportCliError('checkpoint', '导入断点无法读取。');
- }
- if (
- checkpoint?.version !== identity.version
- || checkpoint?.source !== identity.source
- || Number(checkpoint?.targetUserId) !== Number(identity.targetUserId)
- || checkpoint?.snapshotId !== identity.snapshotId
- || typeof checkpoint?.lastSourceKey !== 'string'
- ) {
- throw new VestaImportCliError('checkpoint', '导入断点与本次任务不匹配。');
- }
- return checkpoint;
- }
- export function sanitizeImportReport(report) {
- return {
- dryRun: Boolean(report?.dryRun),
- domains: Number(report?.domains || 0),
- mailboxes: Number(report?.mailboxes || 0),
- messages: Number(report?.messages || 0),
- bytes: Number(report?.bytes || 0),
- plannedMessages: Number(report?.plannedMessages || 0),
- importedMessages: Number(report?.importedMessages || 0),
- skippedMessages: Number(report?.skippedMessages || 0),
- resumeSkippedMessages: Number(report?.resumeSkippedMessages || 0),
- warningCount: Number(report?.warningCount ?? (Array.isArray(report?.warnings) ? report.warnings.length : 0))
- };
- }
- export function usageText() {
- return [
- 'Usage: node scripts/import-vesta-maildir.js --snapshot PATH --data-dir PATH --user USER --source NAME [--checkpoint FILE] [--dry-run]',
- '',
- 'USER accepts a username/email, or an explicit numeric id in the form id:123.',
- 'Run --dry-run first. The checkpoint is written only during a real import.'
- ].join('\n');
- }
- async function main() {
- let options;
- try {
- loadMailHubEnvironment();
- options = parseVestaImportArguments(process.argv.slice(2));
- if (options.help) {
- process.stdout.write(`${usageText()}\n`);
- return;
- }
- const controller = new AbortController();
- const abort = () => controller.abort(new Error('Vesta 导入已取消。'));
- process.once('SIGINT', abort);
- process.once('SIGTERM', abort);
- try {
- await runVestaMaildirImport(options, { signal: controller.signal });
- } finally {
- process.off('SIGINT', abort);
- process.off('SIGTERM', abort);
- }
- } catch (error) {
- const category = safeCategory(error?.category);
- process.stderr.write(`failed errors=1 category=${category}\n`);
- process.exitCode = 1;
- }
- }
- function normalizeRunOptions(options = {}) {
- const source = normalizeImportSource(options.source);
- const snapshot = path.resolve(String(options.snapshot || ''));
- const dataDir = path.resolve(String(options.dataDir || ''));
- const checkpoint = path.resolve(String(options.checkpoint || path.join(dataDir, 'vesta-maildir-import.checkpoint.json')));
- const user = String(options.user || '').trim();
- if (!source || !user || !options.snapshot || !options.dataDir) {
- throw new VestaImportCliError('usage', '导入参数不完整。');
- }
- return { source, snapshot, dataDir, checkpoint, user, dryRun: Boolean(options.dryRun) };
- }
- function resolveDbUser(dbApi, identifier) {
- const target = parseTargetUser(identifier);
- return target.id ? dbApi.getUser(target.id) : dbApi.getUserByLogin(target.login);
- }
- function parseTargetUser(value) {
- const clean = String(value || '').trim().toLowerCase();
- const explicitId = clean.match(/^id:(\d+)$/);
- return explicitId
- ? { id: Number(explicitId[1]), login: '' }
- : { id: 0, login: clean };
- }
- function domainDefaults(env) {
- const policy = String(env.DMARC_POLICY || 'none').trim().toLowerCase();
- return {
- senderHost: String(env.MAIL_HOSTNAME || 'mailhub.local').trim().toLowerCase(),
- sendingIp: String(env.SENDING_IP || '').trim(),
- spfExtra: String(env.DEFAULT_SPF_MECHANISMS || 'include:spf.mailjet.com').trim(),
- dmarcPolicy: ['none', 'quarantine', 'reject'].includes(policy) ? policy : 'none',
- dmarcRua: String(env.DMARC_RUA || '').trim()
- };
- }
- function validNewDomainDefaults(defaults) {
- return validHostname(defaults.senderHost) && Boolean(String(defaults.sendingIp || '').trim());
- }
- function validHostname(value) {
- const hostname = String(value || '').trim().toLowerCase().replace(/\.$/, '');
- if (!hostname || hostname.length > 253) return false;
- return hostname.split('.').every((label) => (
- label.length > 0
- && label.length <= 63
- && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(label)
- ));
- }
- function mailHubSecret(env) {
- if (env.SESSION_SECRET) return String(env.SESSION_SECRET);
- return crypto
- .createHash('sha256')
- .update(`${env.ADMIN_PASSWORD || 'change-this-admin-password'}:${env.API_TOKEN || ''}`)
- .digest('hex');
- }
- function defaultSelector() {
- const date = new Date();
- return `mh${date.getUTCFullYear()}${String(date.getUTCMonth() + 1).padStart(2, '0')}`;
- }
- function snapshotIdentifier(snapshotPath, snapshot = {}) {
- const fingerprint = {
- path: path.resolve(snapshotPath),
- domains: Array.isArray(snapshot.domains) ? snapshot.domains : [],
- mailboxes: Array.isArray(snapshot.mailboxes) ? snapshot.mailboxes : [],
- warnings: Array.isArray(snapshot.warnings) ? snapshot.warnings : []
- };
- return crypto
- .createHash('sha256')
- .update(JSON.stringify(sortJsonValue(fingerprint)))
- .digest('hex');
- }
- function sortJsonValue(value) {
- if (Array.isArray(value)) return value.map(sortJsonValue);
- if (!value || typeof value !== 'object') return value;
- return Object.fromEntries(
- Object.keys(value)
- .sort()
- .map((key) => [key, sortJsonValue(value[key])])
- );
- }
- function normalizeImportSource(value) {
- const source = String(value || '').trim().toLowerCase();
- if (!source || source.length > 120 || !/^[a-z0-9][a-z0-9._:@/-]*$/.test(source)) return '';
- return source;
- }
- function normalizeDomainName(value) {
- const domain = String(value || '').trim().toLowerCase();
- return /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(domain)
- ? domain
- : '';
- }
- function normalizeMailboxAddress(value) {
- const address = String(value || '').trim().toLowerCase();
- return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(address) ? address : '';
- }
- function toCamelCase(value) {
- return String(value).replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase());
- }
- function writeCompletionLine(output, summary) {
- writeCountLine(output, 'complete', {
- dry_run: summary.dryRun ? 1 : 0,
- domains: summary.domains,
- mailboxes: summary.mailboxes,
- messages: summary.messages,
- bytes: summary.bytes,
- planned: summary.plannedMessages,
- imported: summary.importedMessages,
- skipped: summary.skippedMessages,
- resume_skipped: summary.resumeSkippedMessages,
- warnings: summary.warningCount
- });
- }
- function writeCountLine(output, event, values) {
- const fields = Object.entries(values).map(([key, value]) => `${key}=${Number(value || 0)}`);
- output.write(`${event} ${fields.join(' ')}\n`);
- }
- function safeCategory(value) {
- const clean = String(value || 'import').trim().toLowerCase();
- return /^[a-z][a-z0-9_-]{0,31}$/.test(clean) ? clean : 'import';
- }
- async function atomicWriteJson(filePath, value) {
- const directory = path.dirname(filePath);
- await mkdir(directory, { recursive: true });
- const temporary = path.join(
- directory,
- `.${path.basename(filePath)}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`
- );
- try {
- await writeFile(temporary, `${JSON.stringify(value)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
- await rename(temporary, filePath);
- } catch (error) {
- await rm(temporary, { force: true });
- throw error;
- }
- }
- const invokedUrl = process.argv[1] ? pathToFileURL(path.resolve(process.argv[1])).href : '';
- if (import.meta.url === invokedUrl) await main();
|