328 lines
11 KiB
TypeScript
328 lines
11 KiB
TypeScript
/**
|
|
* 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<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;
|
|
},
|
|
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<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 {
|
|
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<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> {
|
|
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<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>`,
|
|
};
|
|
}
|