feat: Add comprehensive SMTP test suite for Deno

- Implemented SMTP client utilities in `test/helpers/smtp.client.ts` for creating test clients, sending emails, and testing connections.
- Developed SMTP protocol test utilities in `test/helpers/utils.ts` for managing TCP connections, sending commands, and handling responses.
- Created a detailed README in `test/readme.md` outlining the test framework, infrastructure, organization, and running instructions.
- Ported CMD-01: EHLO Command tests in `test/suite/smtpserver_commands/test.cmd-01.ehlo-command.test.ts` with multiple scenarios including valid and invalid hostnames.
- Ported CMD-02: MAIL FROM Command tests in `test/suite/smtpserver_commands/test.cmd-02.mail-from.test.ts` covering valid address acceptance, invalid address rejection, SIZE parameter support, and command sequence enforcement.
This commit is contained in:
2025-10-25 15:05:11 +00:00
parent d7f37afc30
commit 1698df3a53
12 changed files with 1668 additions and 3 deletions

View File

@@ -0,0 +1,326 @@
/**
* 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) => {},
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>`,
};
}

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

@@ -0,0 +1,236 @@
/**
* Test SMTP Client Utilities for Deno
* Provides helpers for creating and testing SMTP client functionality
*/
import { smtpClientMod } from '../../ts/mail/delivery/index.ts';
import type { ISmtpClientOptions } from '../../ts/mail/delivery/smtpclient/interfaces.ts';
import type { SmtpClient } from '../../ts/mail/delivery/smtpclient/smtp-client.ts';
import { Email } from '../../ts/mail/core/classes.email.ts';
/**
* Create a test SMTP client with sensible defaults
*/
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,
connectionTimeout: options.connectionTimeout || 5000,
socketTimeout: options.socketTimeout || 5000,
maxConnections: options.maxConnections || 5,
maxMessages: options.maxMessages || 100,
debug: options.debug || false,
tls: options.tls || {
rejectUnauthorized: false,
},
pool: options.pool || false,
};
return smtpClientMod.createSmtpClient(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,
};
const email = new Email({
from: mailOptions.from,
to: mailOptions.to,
subject: mailOptions.subject,
text: mailOptions.text,
html: mailOptions.html,
});
return client.sendMail(email);
}
/**
* 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,
method: authMethod,
},
secure: false,
});
}
/**
* Create TLS-enabled SMTP client
*/
export function createTlsClient(
host: string,
port: number,
options: {
secure?: boolean;
rejectUnauthorized?: boolean;
} = {}
): SmtpClient {
return createTestSmtpClient({
host,
port,
secure: options.secure || 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,
};
}
/**
* Create a pooled SMTP client for concurrent testing
*/
export function createPooledTestClient(
options: Partial<ISmtpClientOptions> = {}
): SmtpClient {
return createTestSmtpClient({
...options,
pool: true,
maxConnections: options.maxConnections || 5,
maxMessages: options.maxMessages || 100,
});
}

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

@@ -0,0 +1,350 @@
/**
* SMTP Test Utilities for Deno
* Provides helper functions for testing SMTP protocol implementation
*/
import { net } from '../../ts/plugins.ts';
/**
* 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
*/
export async function connectToSmtp(
host: string,
port: number,
timeout: number = 5000
): Promise<Deno.TcpConn> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const conn = await Deno.connect({
hostname: host,
port,
transport: 'tcp',
});
clearTimeout(timeoutId);
return conn;
} catch (error) {
clearTimeout(timeoutId);
if (error instanceof Error && error.name === 'AbortError') {
throw new Error(`Connection timeout after ${timeout}ms`);
}
throw error;
}
}
/**
* Read data from TCP connection with timeout
*/
async function readWithTimeout(
conn: Deno.TcpConn,
timeout: number
): Promise<string> {
const buffer = new Uint8Array(4096);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const n = await conn.read(buffer);
clearTimeout(timeoutId);
if (n === null) {
throw new Error('Connection closed');
}
const decoder = new TextDecoder();
return decoder.decode(buffer.subarray(0, n));
} catch (error) {
clearTimeout(timeoutId);
if (error instanceof Error && error.name === 'AbortError') {
throw new Error(`Read timeout after ${timeout}ms`);
}
throw error;
}
}
/**
* Send SMTP command and wait for response
*/
export async function sendSmtpCommand(
conn: Deno.TcpConn,
command: string,
expectedCode?: string,
timeout: number = 5000
): Promise<string> {
// Send command
const encoder = new TextEncoder();
await conn.write(encoder.encode(command + '\r\n'));
// Read response
let buffer = '';
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
const chunk = await readWithTimeout(conn, timeout - (Date.now() - startTime));
buffer += chunk;
// Check if we have a complete response (ends with \r\n)
if (buffer.includes('\r\n')) {
if (expectedCode && !buffer.startsWith(expectedCode)) {
throw new Error(`Expected ${expectedCode}, got: ${buffer.trim()}`);
}
return buffer;
}
}
throw new Error(`Command timeout after ${timeout}ms`);
}
/**
* Wait for SMTP greeting (220 code)
*/
export async function waitForGreeting(
conn: Deno.TcpConn,
timeout: number = 5000
): Promise<string> {
let buffer = '';
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
const chunk = await readWithTimeout(conn, timeout - (Date.now() - startTime));
buffer += chunk;
if (buffer.includes('220')) {
return buffer;
}
}
throw new Error(`Greeting timeout after ${timeout}ms`);
}
/**
* Perform SMTP handshake and return capabilities
*/
export async function performSmtpHandshake(
conn: Deno.TcpConn,
hostname: string = 'test.example.com'
): Promise<string[]> {
const capabilities: string[] = [];
// Wait for greeting
await waitForGreeting(conn);
// Send EHLO
const ehloResponse = await sendSmtpCommand(conn, `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<Deno.TcpConn[]> {
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(conn: Deno.TcpConn): Promise<void> {
try {
await sendSmtpCommand(conn, 'QUIT', '221');
} catch {
// Ignore errors during QUIT
}
try {
conn.close();
} catch {
// Ignore close errors
}
}
/**
* 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
const encoder = new TextEncoder();
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';
// Convert to base64
const bytes = encoder.encode(attachment.content);
const base64 = btoa(String.fromCharCode(...bytes));
message += 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!;
}