import * as plugins from '../../ts/plugins.js'; import { UnifiedEmailServer } from '../../ts/mail/routing/classes.unified.email.server.js'; import { createSmtpServer } from '../../ts/mail/delivery/smtpserver/index.js'; import type { ISmtpServerOptions } from '../../ts/mail/delivery/smtpserver/interfaces.js'; import type { net } from '../../ts/plugins.js'; export interface ITestServerConfig { port: number; hostname?: string; tlsEnabled?: boolean; authRequired?: boolean; timeout?: number; testCertPath?: string; testKeyPath?: string; maxConnections?: number; size?: number; maxRecipients?: number; } export interface ITestServer { server: any; smtpServer: any; port: number; hostname: string; config: ITestServerConfig; startTime: number; } /** * Starts a test SMTP server with the given configuration */ export async function startTestServer(config: ITestServerConfig): Promise { const serverConfig = { port: config.port || 2525, hostname: config.hostname || 'localhost', tlsEnabled: config.tlsEnabled || false, authRequired: config.authRequired || false, timeout: config.timeout || 30000, maxConnections: config.maxConnections || 100, size: config.size || 10 * 1024 * 1024, // 10MB default maxRecipients: config.maxRecipients || 100 }; // Create a mock email server for testing const mockEmailServer = { processEmailByMode: async (emailData: any) => { console.log(`📧 [Test Server] Processing email:`, emailData.subject || 'No subject'); return emailData; }, getRateLimiter: () => { // Return a mock rate limiter for testing return { recordConnection: (_ip: string) => ({ allowed: true, remaining: 100 }), checkConnectionLimit: async (_ip: string) => ({ allowed: true, remaining: 100 }), checkMessageLimit: (_senderAddress: string, _ip: string, _recipientCount?: number, _pattern?: string, _domain?: string) => ({ allowed: true, remaining: 1000 }), checkRecipientLimit: async (_session: any) => ({ allowed: true, remaining: 50 }), recordAuthenticationFailure: async (_ip: string) => {}, recordSyntaxError: async (_ip: string) => {}, recordCommandError: async (_ip: string) => {}, isBlocked: async (_ip: string) => false, cleanup: async () => {} }; } } as any; // Load test certificates let key: string; let cert: string; if (serverConfig.tlsEnabled) { try { const certPath = config.testCertPath || './test/fixtures/test-cert.pem'; const keyPath = config.testKeyPath || './test/fixtures/test-key.pem'; cert = await plugins.fs.promises.readFile(certPath, 'utf8'); key = await plugins.fs.promises.readFile(keyPath, 'utf8'); } catch (error) { console.warn('⚠️ Failed to load TLS certificates, falling back to self-signed'); // Generate self-signed certificate for testing const forge = await import('node-forge'); const pki = forge.default.pki; // Generate key pair const keys = pki.rsa.generateKeyPair(2048); // Create certificate const certificate = pki.createCertificate(); certificate.publicKey = keys.publicKey; certificate.serialNumber = '01'; certificate.validity.notBefore = new Date(); certificate.validity.notAfter = new Date(); certificate.validity.notAfter.setFullYear(certificate.validity.notBefore.getFullYear() + 1); const attrs = [{ name: 'commonName', value: serverConfig.hostname }]; certificate.setSubject(attrs); certificate.setIssuer(attrs); certificate.sign(keys.privateKey); // Convert to PEM cert = pki.certificateToPem(certificate); key = pki.privateKeyToPem(keys.privateKey); } } else { // Always provide a self-signed certificate for non-TLS servers // This is required by the interface const forge = await import('node-forge'); const pki = forge.default.pki; // Generate key pair const keys = pki.rsa.generateKeyPair(2048); // Create certificate const certificate = pki.createCertificate(); certificate.publicKey = keys.publicKey; certificate.serialNumber = '01'; certificate.validity.notBefore = new Date(); certificate.validity.notAfter = new Date(); certificate.validity.notAfter.setFullYear(certificate.validity.notBefore.getFullYear() + 1); const attrs = [{ name: 'commonName', value: serverConfig.hostname }]; certificate.setSubject(attrs); certificate.setIssuer(attrs); certificate.sign(keys.privateKey); // Convert to PEM cert = pki.certificateToPem(certificate); key = pki.privateKeyToPem(keys.privateKey); } // SMTP server options const smtpOptions: ISmtpServerOptions = { port: serverConfig.port, hostname: serverConfig.hostname, key: key, cert: cert, maxConnections: serverConfig.maxConnections, size: serverConfig.size, maxRecipients: serverConfig.maxRecipients, socketTimeout: serverConfig.timeout, connectionTimeout: serverConfig.timeout * 2, cleanupInterval: 300000, auth: serverConfig.authRequired ? ({ required: true, methods: ['PLAIN', 'LOGIN'] as ('PLAIN' | 'LOGIN' | 'OAUTH2')[], validateUser: async (username: string, password: string) => { // Test server accepts these credentials return username === 'testuser' && password === 'testpass'; } } as any) : undefined }; // Create SMTP server const smtpServer = await createSmtpServer(mockEmailServer, smtpOptions); // Start the server await smtpServer.listen(); // Wait for server to be ready await waitForServerReady(serverConfig.hostname, serverConfig.port); console.log(`✅ Test SMTP server started on ${serverConfig.hostname}:${serverConfig.port}`); return { server: mockEmailServer, smtpServer: smtpServer, port: serverConfig.port, hostname: serverConfig.hostname, config: serverConfig, startTime: Date.now() }; } /** * Stops a test SMTP server */ export async function stopTestServer(testServer: ITestServer): Promise { if (!testServer || !testServer.smtpServer) { console.warn('⚠️ No test server to stop'); return; } try { console.log(`🛑 Stopping test SMTP server on ${testServer.hostname}:${testServer.port}`); // Stop the SMTP server if (testServer.smtpServer.close && typeof testServer.smtpServer.close === 'function') { await testServer.smtpServer.close(); } // Wait for port to be free await waitForPortFree(testServer.port); console.log(`✅ Test SMTP server stopped`); } catch (error) { console.error('❌ Error stopping test server:', error); throw error; } } /** * Wait for server to be ready to accept connections */ async function waitForServerReady(hostname: string, port: number, timeout: number = 10000): Promise { const startTime = Date.now(); while (Date.now() - startTime < timeout) { try { await new Promise((resolve, reject) => { const socket = plugins.net.createConnection({ port, host: hostname }); socket.on('connect', () => { socket.end(); resolve(); }); socket.on('error', reject); setTimeout(() => { socket.destroy(); reject(new Error('Connection timeout')); }, 1000); }); return; // Server is ready } catch { // Server not ready yet, wait and retry await new Promise(resolve => setTimeout(resolve, 100)); } } throw new Error(`Server did not become ready within ${timeout}ms`); } /** * Wait for port to be free */ async function waitForPortFree(port: number, timeout: number = 5000): Promise { const startTime = Date.now(); while (Date.now() - startTime < timeout) { const isFree = await isPortFree(port); if (isFree) { return; } await new Promise(resolve => setTimeout(resolve, 100)); } console.warn(`⚠️ Port ${port} still in use after ${timeout}ms`); } /** * Check if a port is free */ async function isPortFree(port: number): Promise { return new Promise((resolve) => { const server = plugins.net.createServer(); server.listen(port, () => { server.close(() => resolve(true)); }); server.on('error', () => resolve(false)); }); } /** * Get an available port for testing */ export async function getAvailablePort(startPort: number = 25000): Promise { for (let port = startPort; port < startPort + 1000; port++) { if (await isPortFree(port)) { return port; } } throw new Error(`No available ports found starting from ${startPort}`); } /** * Create test email data */ export function createTestEmail(options: { from?: string; to?: string | string[]; subject?: string; text?: string; html?: string; attachments?: any[]; } = {}): any { return { from: options.from || 'test@example.com', to: options.to || 'recipient@example.com', subject: options.subject || 'Test Email', text: options.text || 'This is a test email', html: options.html || '

This is a test email

', attachments: options.attachments || [], date: new Date(), messageId: `<${Date.now()}@test.example.com>` }; } /** * Simple test server for custom protocol testing */ export interface ISimpleTestServer { server: any; hostname: string; port: number; } export async function createTestServer(options: { onConnection?: (socket: any) => void | Promise; port?: number; hostname?: string; }): Promise { const hostname = options.hostname || 'localhost'; const port = options.port || await getAvailablePort(); const server = plugins.net.createServer((socket) => { if (options.onConnection) { const result = options.onConnection(socket); if (result && typeof result.then === 'function') { result.catch(error => { console.error('Error in onConnection handler:', error); socket.destroy(); }); } } }); return new Promise((resolve, reject) => { server.listen(port, hostname, () => { resolve({ server, hostname, port }); }); server.on('error', reject); }); }