311 lines
8.1 KiB
TypeScript
311 lines
8.1 KiB
TypeScript
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!;
|
|
} |