333 lines
9.5 KiB
TypeScript
333 lines
9.5 KiB
TypeScript
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<ITestServer> {
|
|
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;
|
|
}
|
|
} 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';
|
|
}
|
|
} : 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<void> {
|
|
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<void> {
|
|
const startTime = Date.now();
|
|
|
|
while (Date.now() - startTime < timeout) {
|
|
try {
|
|
await new Promise<void>((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<void> {
|
|
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<boolean> {
|
|
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<number> {
|
|
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 || '<p>This is a test email</p>',
|
|
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<void>;
|
|
port?: number;
|
|
hostname?: string;
|
|
}): Promise<ISimpleTestServer> {
|
|
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);
|
|
});
|
|
} |