UNPKG

maildev

Version:

SMTP Server and Web Interface for reading and testing emails during development

286 lines 9.31 kB
/** * MailDev Server Orchestrator * * Main class that manages all MailDev servers (Storage, SMTP, API) */ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { readFile } from 'node:fs/promises'; import { MemoryStorage, FileStorage } from '@maildev/core'; import { createSMTPServer } from '@maildev/smtp'; import { createAPIServer } from '@maildev/api'; import { registerUI } from '@maildev/ui/server'; /** * Server Orchestrator * * Creates and manages Storage, SMTP, and API servers */ export class Orchestrator { config; logger; storage = null; smtp = null; api = null; running = false; constructor(options) { this.config = options.config; this.logger = options.logger; } /** * Start all servers */ async start() { if (this.running) { throw new Error('Servers are already running'); } // 1. Create and initialize storage this.storage = await this.createStorage(); await this.storage.initialize(); this.logger.debug('Storage initialized'); // 2. Create mail directory path const mailDir = this.config.mailDirectory || join(tmpdir(), 'maildev'); // 3. Create SMTP server const smtpOptions = { port: this.config.smtp, host: this.config.ip, storage: this.storage, mailDir, logger: this.config.verbose ? this.createSmtpLogger() : false, }; if (this.config.incomingSecure !== undefined) { smtpOptions.secure = this.config.incomingSecure; } if (this.config.incomingCert) { smtpOptions.certPath = this.config.incomingCert; } if (this.config.incomingKey) { smtpOptions.keyPath = this.config.incomingKey; } const smtpAuth = this.getSmtpAuth(); if (smtpAuth) { smtpOptions.auth = smtpAuth; } if (this.config.hideExtensions) { smtpOptions.hideExtensions = this.config.hideExtensions; } this.smtp = createSMTPServer(smtpOptions); // 4. Configure relay if outgoing host is set if (this.config.outgoingHost) { const relayConfig = this.getRelayConfig(); this.smtp.setupRelay(relayConfig); this.logger.debug(`Relay configured to ${relayConfig.host}:${relayConfig.port}`); } // 5. Configure auto-relay if enabled if (this.config.autoRelay) { const autoRelayConfig = await this.getAutoRelayConfig(); this.smtp.setAutoRelay(autoRelayConfig); this.logger.debug('Auto-relay enabled'); } // 6. Start SMTP server await this.smtp.start(); this.logger.debug(`SMTP server started on ${this.config.ip}:${this.config.smtp}`); // 7. Set up email event handlers this.setupEmailHandlers(); // 8. Create and start API server (unless disabled) if (!this.config.disableWeb) { // Normalize basePath: '/' should become empty string, remove trailing slashes const basePath = this.config.basePathname === '/' ? '' : this.config.basePathname.replace(/\/$/, ''); const apiOptions = { port: this.config.web, host: this.config.webIp, basePath, storage: this.storage, smtp: this.smtp, }; const webAuth = this.getWebAuth(); if (webAuth) { apiOptions.auth = webAuth; } if (this.config.verbose !== undefined) { apiOptions.logger = this.config.verbose; } if (this.config.mcp) { apiOptions.mcp = { enabled: true }; } this.api = createAPIServer(apiOptions); await this.api.registerPlugins(); await registerUI(this.api.server, { basePath }); await this.api.listen(); this.logger.debug(`API server started on ${this.config.webIp}:${this.config.web}`); } this.running = true; const servers = { storage: this.storage, smtp: this.smtp, }; if (this.api) { servers.api = this.api; } return servers; } /** * Stop all servers gracefully */ async stop() { if (!this.running) return; this.logger.debug('Stopping servers...'); // Stop in reverse order if (this.api) { await this.api.stop(); this.logger.debug('API server stopped'); } if (this.smtp) { await this.smtp.stop(); this.logger.debug('SMTP server stopped'); } if (this.storage) { await this.storage.close(); this.logger.debug('Storage closed'); } this.running = false; this.api = null; this.smtp = null; this.storage = null; } /** * Check if servers are running */ isRunning() { return this.running; } /** * Get server instances */ getServers() { if (!this.running || !this.storage || !this.smtp) { return null; } const servers = { storage: this.storage, smtp: this.smtp, }; if (this.api) { servers.api = this.api; } return servers; } /** * Create storage instance based on config */ async createStorage() { if (this.config.mailDirectory) { this.logger.debug(`Using file storage: ${this.config.mailDirectory}`); return new FileStorage({ mailDirectory: this.config.mailDirectory, }); } this.logger.debug('Using in-memory storage'); return new MemoryStorage(); } /** * Get SMTP authentication config */ getSmtpAuth() { if (this.config.incomingUser && this.config.incomingPass) { return { user: this.config.incomingUser, pass: this.config.incomingPass, }; } return undefined; } /** * Get web authentication config */ getWebAuth() { if (this.config.webUser && this.config.webPass) { return { type: 'basic', user: this.config.webUser, pass: this.config.webPass, }; } return undefined; } /** * Get relay configuration */ getRelayConfig() { const config = { host: this.config.outgoingHost, port: this.config.outgoingPort || 25, }; if (this.config.outgoingSecure !== undefined) { config.secure = this.config.outgoingSecure; } if (this.config.outgoingUser && this.config.outgoingPass) { config.auth = { user: this.config.outgoingUser, pass: this.config.outgoingPass, }; } return config; } /** * Get auto-relay configuration */ async getAutoRelayConfig() { const config = { enabled: true, }; // If auto-relay is a string, it's the default recipient if (typeof this.config.autoRelay === 'string') { config.defaultRecipient = this.config.autoRelay; } // Load rules file if specified if (this.config.autoRelayRules) { try { const rulesContent = await readFile(this.config.autoRelayRules, 'utf-8'); config.rules = JSON.parse(rulesContent); } catch (error) { const message = error instanceof Error ? error.message : String(error); throw new Error(`Failed to load auto-relay rules: ${message}`); } } return config; } /** * Create SMTP logger adapter */ createSmtpLogger() { return { log: (...args) => this.logger.debug(String(args[0]), ...args.slice(1)), info: (...args) => this.logger.info(String(args[0]), ...args.slice(1)), warn: (...args) => this.logger.warn(String(args[0]), ...args.slice(1)), error: (...args) => this.logger.error(String(args[0]), ...args.slice(1)), debug: (...args) => this.logger.debug(String(args[0]), ...args.slice(1)), }; } /** * Set up email event handlers */ setupEmailHandlers() { if (!this.smtp) return; this.smtp.on('new', (email) => { const from = email.from?.[0]?.address || 'unknown'; const to = email.to?.[0]?.address || 'unknown'; this.logger.email(from, to, email.subject || '(no subject)'); if (this.config.logMailContents) { this.logger.emailContents(email); } }); this.smtp.on('error', (error) => { this.logger.error(`SMTP error: ${error.message}`); }); } } /** * Create a new orchestrator instance */ export function createOrchestrator(options) { return new Orchestrator(options); } //# sourceMappingURL=orchestrator.js.map