/** * Test SMTP Server Loader for Deno * Manages test server lifecycle and configuration */ import { createSmtpServer } from '../../ts/mail/delivery/smtpserver/index.ts'; import type { ISmtpServerOptions } from '../../ts/mail/delivery/smtpserver/interfaces.ts'; import { Email } from '../../ts/mail/core/classes.email.ts'; import { net, crypto } from '../../ts/plugins.ts'; 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; } /** * Generate self-signed certificate for testing * Uses Deno's built-in crypto for key generation */ async function generateSelfSignedCert(hostname: string): Promise<{ key: string; cert: string; }> { // For now, return placeholder cert/key that will be replaced with real generation // In production tests, we should either use pre-generated test certs from fixtures // or implement proper cert generation using Deno's crypto API // This is a self-signed test certificate - DO NOT use in production const key = `-----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEAxzYIwlfnr7AK2v6E+c2oYD7nAIXIIvDuvVvZ8R9kyxXIzTXB j5D1AgntqKS3bFR1XT8hCVeXjuLKPBvXbhVjG15gXlXxpNiFi1ZcphJvs4zB/Vh7 Zv2ALt3anSIwsJ2rZA/R/GqdJPkHvYf/GMTDLw0YllR0YOevErnRIIM5S58Lj2nT Cr5v5hK1Gl9mWwRkFQKkWVl2UXt/JX6C7Z6UyJXMZSnoG0Kw6GQje41K5r0Zdzrh rGfmb9wSDUn9sZGX6il+oMiYz7UgQkPEzGUZEJxKJwxy8ZgPdSgbvYq4WwPwbBUJ lpw0gt5i6HOS7CphRama+zAf5LvfSLoLXSP5JwIDAQABAoIBAQC8C5Ge6wS4LuH9 tbZFPwjdGHXL+QT2fOFxPBrE7PkeY8UXD7G5Yei6iqqCxJh8nhLQ3DoayhZM69hO ePOV1Z/LDERCnGel15WKQ1QJ1HZ+JQXnfQrE1Mi9QrXO5bVFtnXIr0mZ+AzwoUmn K5fYCvaL3xDZPDzOYL5kZG2hQKgbywGKZoQx16G0dSEhlAHbK9z6XmPRrbUKGzB8 qV7QGbL7BUTQs5JW/8LpkYr5C0q5THtUVb9mHNR3jPf9WTPQ0D3lxcbLS4PQ8jQ/ L/GcuHGmsXhe2Unw3w2wpuJKPeHKz4rBNIvaSjIZl9/dIKM88JYQTiIGKErxsC0e kczQMp6BAoGBAO0zUN8H7ynXGNNtK/tJo0lI3qg1ZKgr+0CU2L5eU8Bn1oJ1JkCI WD3p36NdECx5tGexm9U6MN+HzKYUjnQ6LKzbHQGLZqzF5IL5axXgCn8w4BM+6Ixm y8kQgsTKlKRMXIn8RZCmXNnc7v0FhBgpDxPmm7ZUuOPrInd8Ph4mEsePAoGBANb4 3/izAHnLEp3/sTOZpfWBnDcvEHCG7/JAX0TDRW1FpXiTHpvDV1j3XU3EvLl7WRJ1 B+B8h/Z6kQtUUxQ3I+zxuQIkQYI8qPu+xhQ8gb5AIO5CMX09+xKUgYjQtm7kYs7W L0LD9u3hkGsJk2wfVvMJKb3OSIHeTwRzFCzGX995AoGADkLB8eu/FKAIfwRPCHVE sfwMtqjkj2XJ9FeNcRQ5g/Tf8OGnCGEzBwXb05wJVrXUgXp4dBaqYTdAKj8uLEvd mi9t/LzR+33cGUdAQHItxcKbsMv00TyNRQUvZFZ7ZEY8aBkv5uZfvJHZ5iQ8C7+g HGXNfVGXGPutz/KN6X25CLECgYEAjVLK0MkXzLxCYJRDIhB1TpQVXjpxYUP2Vxls SSxfeYqkJPgNvYiHee33xQ8+TP1y9WzkWh+g2AbGmwTuKKL6CvQS9gKVvqqaFB7y KrkR13MTPJKvHHdQYKGQqQGgHKh0kGFCC0+PoVwtYs/XU1KpZCE16nNgXrOvTYNN HxESa+kCgYB7WOcawTp3WdKP8JbolxIfxax7Kd4QkZhY7dEb4JxBBYXXXpv/NHE9 pcJw4eKDyY+QE2AHPu3+fQYzXopaaTGRpB+ynEfYfD2hW+HnOWfWu/lFJbiwBn/S wRsYzSWiLtNplKNFRrsSoMWlh8GOTUpZ7FMLXWhE4rE9NskQBbYq8g== -----END RSA PRIVATE KEY-----`; const cert = `-----BEGIN CERTIFICATE----- MIIDazCCAlOgAwIBAgIUcmAewXEYwtzbZmZAJ5inMogKSbowDQYJKoZIhvcNAQEL BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDAyMjAwODM4MzRaFw0yNTAy MTkwODM4MzRaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB AQUAA4IBDwAwggEKAoIBAQDHNgjCV+evsAra/oT5zahgPucAhcgi8O69W9nxH2TL FcjNNcGPkPUCCe2opLdsVHVdPyEJV5eO4so8G9duFWMbXmBeVfGk2IWLVlymEm+z jMH9WHtm/YAu3dqdIjCwnatED9H8ap0k+Qd9h/8YxMMvDRiWVHRg568SudEggzlL nwuPadMKvm/mErUaX2ZbBGQVAqRZWXZRe38lfoLtnpTIlcxlKegbQrDoZCN7jUrm vRl3OuGsZ+Zv3BINSf2xkZfqKX6gyJjPtSBCQ8TMZRkQnEonDHLxmA91KBu9irhb A/BsFQmWnDSC3mLoc5LsKmFFqZr7MB/ku99IugtdI/knAgMBAAGjUzBRMB0GA1Ud DgQWBBQryyWLuN22OqU1r9HIt2tMLBk42DAfBgNVHSMEGDAWgBQryyWLuN22OqU1 r9HIt2tMLBk42DAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAe CeXQZlXJ2xLnDoOoKY3BpodErNmAwygGYxwDCU0xPbpUMPrQhLI80JlZmfy58gT/ 0ZbULS+srShfEsFnBLmzWLGXDvA/IKCQyTmCQwbPeELGXF6h4URMb+lQL7WL9tY0 uUg2dA+7CtYokIrOkGqUitPK3yvVhxugkf51WIgKMACZDibOQSWrV5QO2vHOAaO9 ePzRGGl3+Ebmcs3+5w1fI6OLsIZH10lfEnC83C0lO8tIJlGsXMQkCjAcX22rT0rc AcxLm07H4EwMwgOAJUkuDjD3y4+KH91jKWF8bhaLZooFB8lccNnaCRiuZRnXlvmf M7uVlLGwlj5R9iHd+0dP -----END CERTIFICATE-----`; return { key, cert }; } /** * Start 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) => {}, recordError: (_ip: string) => false, // Returns false = don't block IP in tests isBlocked: async (_ip: string) => false, cleanup: async () => {}, }; }, } as any; // Load or generate test certificates let key: string; let cert: string; if (serverConfig.tlsEnabled && config.testCertPath && config.testKeyPath) { try { key = await Deno.readTextFile(config.testKeyPath); cert = await Deno.readTextFile(config.testCertPath); } catch (error) { console.warn('⚠️ Failed to load TLS certificates, generating self-signed', error); const generated = await generateSelfSignedCert(serverConfig.hostname); key = generated.key; cert = generated.cert; } } else { // Always generate a certificate (required by the interface) const generated = await generateSelfSignedCert(serverConfig.hostname); key = generated.key; cert = generated.cert; } // 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(), }; } /** * Stop 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 { const conn = await Deno.connect({ hostname, port, transport: 'tcp' }); conn.close(); 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 { try { const listener = Deno.listen({ port, transport: 'tcp' }); listener.close(); return true; } catch { return 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>`, }; }