This commit is contained in:
2025-05-23 19:03:44 +00:00
parent 7d28d23bbd
commit 1b141ec8f3
101 changed files with 30736 additions and 374 deletions

View File

@ -0,0 +1,260 @@
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 if TLS is enabled
let key: string | undefined;
let cert: string | undefined;
if (serverConfig.tlsEnabled) {
try {
const certPath = config.testCertPath || '/home/centraluser/eu.central.ingress-2/certs/bleu_de_HTTPS/cert.pem';
const keyPath = config.testKeyPath || '/home/centraluser/eu.central.ingress-2/certs/bleu_de_HTTPS/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.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
};
// Create SMTP server
const smtpServer = await createSmtpServer(mockEmailServer, smtpOptions);
// Start the server
await smtpServer.start();
// 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.stop && typeof testServer.smtpServer.stop === 'function') {
await testServer.smtpServer.stop();
} else if (testServer.smtpServer.close && typeof testServer.smtpServer.close === 'function') {
await new Promise<void>((resolve) => {
testServer.smtpServer.close(() => resolve());
});
}
// 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>`
};
}

212
test/helpers/smtp.client.ts Normal file
View File

@ -0,0 +1,212 @@
import { SmtpClient } from '../../ts/mail/delivery/classes.smtp.client.js';
import type { ISmtpClientOptions } from '../../ts/mail/delivery/smtpclient/interfaces.js';
/**
* Create a test SMTP client
*/
export function createTestSmtpClient(options: Partial<ISmtpClientOptions> = {}): SmtpClient {
const defaultOptions: ISmtpClientOptions = {
host: options.host || 'localhost',
port: options.port || 2525,
secure: options.secure || false,
auth: options.auth,
ignoreTLS: options.ignoreTLS || true,
requireTLS: options.requireTLS || false,
connectionTimeout: options.connectionTimeout || 5000,
socketTimeout: options.socketTimeout || 5000,
greetingTimeout: options.greetingTimeout || 5000,
maxConnections: options.maxConnections || 5,
maxMessages: options.maxMessages || 100,
rateDelta: options.rateDelta || 1000,
rateLimit: options.rateLimit || 5,
logger: options.logger || false,
debug: options.debug || false,
authMethod: options.authMethod || 'PLAIN',
tls: options.tls || {
rejectUnauthorized: false
}
};
return new SmtpClient(defaultOptions);
}
/**
* Send test email using SMTP client
*/
export async function sendTestEmail(
client: SmtpClient,
options: {
from?: string;
to?: string | string[];
subject?: string;
text?: string;
html?: string;
} = {}
): Promise<any> {
const mailOptions = {
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
};
return client.sendMail(mailOptions);
}
/**
* Test SMTP client connection
*/
export async function testClientConnection(
host: string,
port: number,
timeout: number = 5000
): Promise<boolean> {
const client = createTestSmtpClient({
host,
port,
connectionTimeout: timeout
});
try {
const result = await client.verify();
return result;
} catch (error) {
throw error;
} finally {
if (client.close) {
await client.close();
}
}
}
/**
* Create authenticated SMTP client
*/
export function createAuthenticatedClient(
host: string,
port: number,
username: string,
password: string,
authMethod: 'PLAIN' | 'LOGIN' = 'PLAIN'
): SmtpClient {
return createTestSmtpClient({
host,
port,
auth: {
user: username,
pass: password
},
authMethod,
secure: false,
ignoreTLS: true
});
}
/**
* Create TLS-enabled SMTP client
*/
export function createTlsClient(
host: string,
port: number,
options: {
secure?: boolean;
requireTLS?: boolean;
rejectUnauthorized?: boolean;
} = {}
): SmtpClient {
return createTestSmtpClient({
host,
port,
secure: options.secure || false,
requireTLS: options.requireTLS || false,
ignoreTLS: false,
tls: {
rejectUnauthorized: options.rejectUnauthorized || false
}
});
}
/**
* Test client pool status
*/
export async function testClientPoolStatus(client: SmtpClient): Promise<any> {
if (typeof client.getPoolStatus === 'function') {
return client.getPoolStatus();
}
// Fallback for clients without pool status
return {
size: 1,
available: 1,
pending: 0,
connecting: 0,
active: 0
};
}
/**
* Send multiple emails concurrently
*/
export async function sendConcurrentEmails(
client: SmtpClient,
count: number,
emailOptions: {
from?: string;
to?: string;
subject?: string;
text?: string;
} = {}
): Promise<any[]> {
const promises = [];
for (let i = 0; i < count; i++) {
promises.push(
sendTestEmail(client, {
...emailOptions,
subject: `${emailOptions.subject || 'Test Email'} ${i + 1}`
})
);
}
return Promise.all(promises);
}
/**
* Measure client throughput
*/
export async function measureClientThroughput(
client: SmtpClient,
duration: number = 10000,
emailOptions: {
from?: string;
to?: string;
subject?: string;
text?: string;
} = {}
): Promise<{ totalSent: number; successCount: number; errorCount: number; throughput: number }> {
const startTime = Date.now();
let totalSent = 0;
let successCount = 0;
let errorCount = 0;
while (Date.now() - startTime < duration) {
try {
await sendTestEmail(client, emailOptions);
successCount++;
} catch (error) {
errorCount++;
}
totalSent++;
}
const actualDuration = (Date.now() - startTime) / 1000; // in seconds
const throughput = totalSent / actualDuration;
return {
totalSent,
successCount,
errorCount,
throughput
};
}

311
test/helpers/test.utils.ts Normal file
View File

@ -0,0 +1,311 @@
import * as plugins from '../../ts/plugins.js';
/**
* Test result interface
*/
export interface ITestResult {
success: boolean;
duration: number;
message?: string;
error?: string;
details?: any;
}
/**
* Test configuration interface
*/
export interface ITestConfig {
host: string;
port: number;
timeout: number;
fromAddress?: string;
toAddress?: string;
[key: string]: any;
}
/**
* Connect to SMTP server and get greeting
*/
export async function connectToSmtp(host: string, port: number, timeout: number = 5000): Promise<plugins.net.Socket> {
return new Promise((resolve, reject) => {
const socket = plugins.net.createConnection({ host, port });
const timer = setTimeout(() => {
socket.destroy();
reject(new Error(`Connection timeout after ${timeout}ms`));
}, timeout);
socket.once('connect', () => {
clearTimeout(timer);
resolve(socket);
});
socket.once('error', (error) => {
clearTimeout(timer);
reject(error);
});
});
}
/**
* Send SMTP command and wait for response
*/
export async function sendSmtpCommand(
socket: plugins.net.Socket,
command: string,
expectedCode?: string,
timeout: number = 5000
): Promise<string> {
return new Promise((resolve, reject) => {
let buffer = '';
let timer: NodeJS.Timeout;
const onData = (data: Buffer) => {
buffer += data.toString();
// Check if we have a complete response
if (buffer.includes('\r\n')) {
clearTimeout(timer);
socket.removeListener('data', onData);
if (expectedCode && !buffer.startsWith(expectedCode)) {
reject(new Error(`Expected ${expectedCode}, got: ${buffer.trim()}`));
} else {
resolve(buffer);
}
}
};
timer = setTimeout(() => {
socket.removeListener('data', onData);
reject(new Error(`Command timeout after ${timeout}ms`));
}, timeout);
socket.on('data', onData);
socket.write(command + '\r\n');
});
}
/**
* Wait for SMTP greeting
*/
export async function waitForGreeting(socket: plugins.net.Socket, timeout: number = 5000): Promise<string> {
return new Promise((resolve, reject) => {
let buffer = '';
let timer: NodeJS.Timeout;
const onData = (data: Buffer) => {
buffer += data.toString();
if (buffer.includes('220')) {
clearTimeout(timer);
socket.removeListener('data', onData);
resolve(buffer);
}
};
timer = setTimeout(() => {
socket.removeListener('data', onData);
reject(new Error(`Greeting timeout after ${timeout}ms`));
}, timeout);
socket.on('data', onData);
});
}
/**
* Perform SMTP handshake
*/
export async function performSmtpHandshake(
socket: plugins.net.Socket,
hostname: string = 'test.example.com'
): Promise<string[]> {
const capabilities: string[] = [];
// Wait for greeting
await waitForGreeting(socket);
// Send EHLO
const ehloResponse = await sendSmtpCommand(socket, `EHLO ${hostname}`, '250');
// Parse capabilities
const lines = ehloResponse.split('\r\n');
for (const line of lines) {
if (line.startsWith('250-') || line.startsWith('250 ')) {
const capability = line.substring(4).trim();
if (capability) {
capabilities.push(capability);
}
}
}
return capabilities;
}
/**
* Create multiple concurrent connections
*/
export async function createConcurrentConnections(
host: string,
port: number,
count: number,
timeout: number = 5000
): Promise<plugins.net.Socket[]> {
const connectionPromises = [];
for (let i = 0; i < count; i++) {
connectionPromises.push(connectToSmtp(host, port, timeout));
}
return Promise.all(connectionPromises);
}
/**
* Close SMTP connection gracefully
*/
export async function closeSmtpConnection(socket: plugins.net.Socket): Promise<void> {
try {
await sendSmtpCommand(socket, 'QUIT', '221');
} catch {
// Ignore errors during QUIT
}
socket.destroy();
}
/**
* Generate random email content
*/
export function generateRandomEmail(size: number = 1024): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 \r\n';
let content = '';
for (let i = 0; i < size; i++) {
content += chars.charAt(Math.floor(Math.random() * chars.length));
}
return content;
}
/**
* Create MIME message
*/
export function createMimeMessage(options: {
from: string;
to: string;
subject: string;
text?: string;
html?: string;
attachments?: Array<{ filename: string; content: string; contentType: string }>;
}): string {
const boundary = `----=_Part_${Date.now()}_${Math.random().toString(36).substring(2)}`;
const date = new Date().toUTCString();
let message = '';
message += `From: ${options.from}\r\n`;
message += `To: ${options.to}\r\n`;
message += `Subject: ${options.subject}\r\n`;
message += `Date: ${date}\r\n`;
message += `MIME-Version: 1.0\r\n`;
if (options.attachments && options.attachments.length > 0) {
message += `Content-Type: multipart/mixed; boundary="${boundary}"\r\n`;
message += '\r\n';
// Text part
if (options.text) {
message += `--${boundary}\r\n`;
message += 'Content-Type: text/plain; charset=utf-8\r\n';
message += 'Content-Transfer-Encoding: 8bit\r\n';
message += '\r\n';
message += options.text + '\r\n';
}
// HTML part
if (options.html) {
message += `--${boundary}\r\n`;
message += 'Content-Type: text/html; charset=utf-8\r\n';
message += 'Content-Transfer-Encoding: 8bit\r\n';
message += '\r\n';
message += options.html + '\r\n';
}
// Attachments
for (const attachment of options.attachments) {
message += `--${boundary}\r\n`;
message += `Content-Type: ${attachment.contentType}\r\n`;
message += `Content-Disposition: attachment; filename="${attachment.filename}"\r\n`;
message += 'Content-Transfer-Encoding: base64\r\n';
message += '\r\n';
message += Buffer.from(attachment.content).toString('base64') + '\r\n';
}
message += `--${boundary}--\r\n`;
} else if (options.html && options.text) {
const altBoundary = `----=_Alt_${Date.now()}_${Math.random().toString(36).substring(2)}`;
message += `Content-Type: multipart/alternative; boundary="${altBoundary}"\r\n`;
message += '\r\n';
// Text part
message += `--${altBoundary}\r\n`;
message += 'Content-Type: text/plain; charset=utf-8\r\n';
message += 'Content-Transfer-Encoding: 8bit\r\n';
message += '\r\n';
message += options.text + '\r\n';
// HTML part
message += `--${altBoundary}\r\n`;
message += 'Content-Type: text/html; charset=utf-8\r\n';
message += 'Content-Transfer-Encoding: 8bit\r\n';
message += '\r\n';
message += options.html + '\r\n';
message += `--${altBoundary}--\r\n`;
} else if (options.html) {
message += 'Content-Type: text/html; charset=utf-8\r\n';
message += 'Content-Transfer-Encoding: 8bit\r\n';
message += '\r\n';
message += options.html;
} else {
message += 'Content-Type: text/plain; charset=utf-8\r\n';
message += 'Content-Transfer-Encoding: 8bit\r\n';
message += '\r\n';
message += options.text || '';
}
return message;
}
/**
* Measure operation time
*/
export async function measureTime<T>(operation: () => Promise<T>): Promise<{ result: T; duration: number }> {
const startTime = Date.now();
const result = await operation();
const duration = Date.now() - startTime;
return { result, duration };
}
/**
* Retry operation with exponential backoff
*/
export async function retryOperation<T>(
operation: () => Promise<T>,
maxRetries: number = 3,
initialDelay: number = 1000
): Promise<T> {
let lastError: Error;
for (let i = 0; i < maxRetries; i++) {
try {
return await operation();
} catch (error) {
lastError = error as Error;
if (i < maxRetries - 1) {
const delay = initialDelay * Math.pow(2, i);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw lastError!;
}