update
This commit is contained in:
parent
11a2ae6b27
commit
58f4a123d2
@ -0,0 +1,532 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as plugins from './plugins.js';
|
||||
import { createTestServer } from '../../helpers/server.loader.js';
|
||||
import { createSmtpClient } from '../../helpers/smtp.client.js';
|
||||
|
||||
tap.test('CPERF-02: should achieve optimal message throughput', async (tools) => {
|
||||
const testId = 'CPERF-02-message-throughput';
|
||||
console.log(`\n${testId}: Testing message throughput performance...`);
|
||||
|
||||
let scenarioCount = 0;
|
||||
|
||||
// Scenario 1: Sequential message throughput
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing sequential message throughput`);
|
||||
|
||||
let messageCount = 0;
|
||||
const startTime = Date.now();
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 throughput.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-throughput.example.com\r\n');
|
||||
socket.write('250-PIPELINING\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
messageCount++;
|
||||
const elapsed = Date.now() - startTime;
|
||||
const rate = (messageCount / elapsed) * 1000;
|
||||
socket.write(`250 OK: Message ${messageCount} (${rate.toFixed(1)} msg/sec)\r\n`);
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
const messageCount_ = 20;
|
||||
const messages = Array(messageCount_).fill(null).map((_, i) =>
|
||||
new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i + 1}@example.com`],
|
||||
subject: `Sequential throughput test ${i + 1}`,
|
||||
text: `Testing sequential message sending - message ${i + 1}`
|
||||
})
|
||||
);
|
||||
|
||||
console.log(` Sending ${messageCount_} messages sequentially...`);
|
||||
const sequentialStart = Date.now();
|
||||
|
||||
for (const message of messages) {
|
||||
await smtpClient.sendMail(message);
|
||||
}
|
||||
|
||||
const sequentialTime = Date.now() - sequentialStart;
|
||||
const sequentialRate = (messageCount_ / sequentialTime) * 1000;
|
||||
|
||||
console.log(` Sequential throughput: ${sequentialRate.toFixed(2)} messages/second`);
|
||||
console.log(` Total time: ${sequentialTime}ms for ${messageCount_} messages`);
|
||||
|
||||
expect(sequentialRate).toBeGreaterThan(1); // At least 1 message per second
|
||||
expect(messageCount).toBe(messageCount_);
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 2: Concurrent message throughput
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing concurrent message throughput`);
|
||||
|
||||
let messageCount = 0;
|
||||
const startTime = Date.now();
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 concurrent.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-concurrent.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
messageCount++;
|
||||
const elapsed = Date.now() - startTime;
|
||||
const rate = (messageCount / elapsed) * 1000;
|
||||
socket.write(`250 OK: Message ${messageCount} (${rate.toFixed(1)} msg/sec)\r\n`);
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
const messageCount_ = 30;
|
||||
const messages = Array(messageCount_).fill(null).map((_, i) =>
|
||||
new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i + 1}@example.com`],
|
||||
subject: `Concurrent throughput test ${i + 1}`,
|
||||
text: `Testing concurrent message sending - message ${i + 1}`
|
||||
})
|
||||
);
|
||||
|
||||
console.log(` Sending ${messageCount_} messages concurrently...`);
|
||||
const concurrentStart = Date.now();
|
||||
|
||||
const results = await Promise.all(
|
||||
messages.map(message => smtpClient.sendMail(message))
|
||||
);
|
||||
|
||||
const concurrentTime = Date.now() - concurrentStart;
|
||||
const concurrentRate = (messageCount_ / concurrentTime) * 1000;
|
||||
|
||||
console.log(` Concurrent throughput: ${concurrentRate.toFixed(2)} messages/second`);
|
||||
console.log(` Total time: ${concurrentTime}ms for ${messageCount_} messages`);
|
||||
|
||||
expect(concurrentRate).toBeGreaterThan(5); // Should be faster than sequential
|
||||
expect(results.length).toBe(messageCount_);
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 3: Pipelined message throughput
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing pipelined message throughput`);
|
||||
|
||||
let messageCount = 0;
|
||||
const messageBuffer: string[] = [];
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 pipeline.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const commands = data.toString().split('\r\n').filter(cmd => cmd.length > 0);
|
||||
|
||||
// Process pipelined commands
|
||||
if (commands.length > 1) {
|
||||
console.log(` [Server] Received ${commands.length} pipelined commands`);
|
||||
}
|
||||
|
||||
commands.forEach(command => {
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-pipeline.example.com\r\n');
|
||||
socket.write('250-PIPELINING\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
messageCount++;
|
||||
socket.write(`250 OK: Pipelined message ${messageCount}\r\n`);
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pipelining: true
|
||||
});
|
||||
|
||||
const messageCount_ = 25;
|
||||
const messages = Array(messageCount_).fill(null).map((_, i) =>
|
||||
new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i + 1}@example.com`],
|
||||
subject: `Pipelined throughput test ${i + 1}`,
|
||||
text: `Testing pipelined message sending - message ${i + 1}`
|
||||
})
|
||||
);
|
||||
|
||||
console.log(` Sending ${messageCount_} messages with pipelining...`);
|
||||
const pipelineStart = Date.now();
|
||||
|
||||
const results = await Promise.all(
|
||||
messages.map(message => smtpClient.sendMail(message))
|
||||
);
|
||||
|
||||
const pipelineTime = Date.now() - pipelineStart;
|
||||
const pipelineRate = (messageCount_ / pipelineTime) * 1000;
|
||||
|
||||
console.log(` Pipelined throughput: ${pipelineRate.toFixed(2)} messages/second`);
|
||||
console.log(` Total time: ${pipelineTime}ms for ${messageCount_} messages`);
|
||||
|
||||
expect(pipelineRate).toBeGreaterThan(3); // Should benefit from pipelining
|
||||
expect(results.length).toBe(messageCount_);
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 4: Connection pooling throughput
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing connection pooling throughput`);
|
||||
|
||||
let connectionCount = 0;
|
||||
let messageCount = 0;
|
||||
const connectionMessages = new Map<any, number>();
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
connectionCount++;
|
||||
const connId = connectionCount;
|
||||
connectionMessages.set(socket, 0);
|
||||
|
||||
console.log(` [Server] Connection ${connId} established`);
|
||||
socket.write('220 pool.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('close', () => {
|
||||
const msgCount = connectionMessages.get(socket) || 0;
|
||||
connectionMessages.delete(socket);
|
||||
console.log(` [Server] Connection ${connId} closed after ${msgCount} messages`);
|
||||
});
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-pool.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
messageCount++;
|
||||
const msgCount = (connectionMessages.get(socket) || 0) + 1;
|
||||
connectionMessages.set(socket, msgCount);
|
||||
socket.write(`250 OK: Message ${messageCount} on connection ${connId}\r\n`);
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const pooledClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 5,
|
||||
maxMessages: 100
|
||||
});
|
||||
|
||||
const messageCount_ = 40;
|
||||
const messages = Array(messageCount_).fill(null).map((_, i) =>
|
||||
new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i + 1}@example.com`],
|
||||
subject: `Pooled throughput test ${i + 1}`,
|
||||
text: `Testing connection pooling - message ${i + 1}`
|
||||
})
|
||||
);
|
||||
|
||||
console.log(` Sending ${messageCount_} messages with connection pooling...`);
|
||||
const poolStart = Date.now();
|
||||
|
||||
const results = await Promise.all(
|
||||
messages.map(message => pooledClient.sendMail(message))
|
||||
);
|
||||
|
||||
const poolTime = Date.now() - poolStart;
|
||||
const poolRate = (messageCount_ / poolTime) * 1000;
|
||||
|
||||
console.log(` Pooled throughput: ${poolRate.toFixed(2)} messages/second`);
|
||||
console.log(` Total time: ${poolTime}ms for ${messageCount_} messages`);
|
||||
console.log(` Used ${connectionCount} connections for ${messageCount_} messages`);
|
||||
|
||||
expect(poolRate).toBeGreaterThan(8); // Should be faster with pooling
|
||||
expect(results.length).toBe(messageCount_);
|
||||
expect(connectionCount).toBeGreaterThan(1);
|
||||
expect(connectionCount).toBeLessThanOrEqual(5);
|
||||
|
||||
await pooledClient.close();
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 5: Variable message size throughput
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing variable message size throughput`);
|
||||
|
||||
let totalBytes = 0;
|
||||
let messageCount = 0;
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 variable.example.com ESMTP\r\n');
|
||||
|
||||
let inData = false;
|
||||
let messageSize = 0;
|
||||
|
||||
socket.on('data', (data) => {
|
||||
if (inData) {
|
||||
messageSize += data.length;
|
||||
if (data.toString().includes('\r\n.\r\n')) {
|
||||
inData = false;
|
||||
messageCount++;
|
||||
totalBytes += messageSize;
|
||||
const avgSize = Math.round(totalBytes / messageCount);
|
||||
socket.write(`250 OK: Message ${messageCount} (${messageSize} bytes, avg: ${avgSize})\r\n`);
|
||||
messageSize = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-variable.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
inData = true;
|
||||
messageSize = 0;
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Create messages of varying sizes
|
||||
const messageSizes = [
|
||||
{ size: 'small', content: 'Short message' },
|
||||
{ size: 'medium', content: 'Medium message: ' + 'x'.repeat(1000) },
|
||||
{ size: 'large', content: 'Large message: ' + 'x'.repeat(10000) },
|
||||
{ size: 'extra-large', content: 'Extra large message: ' + 'x'.repeat(50000) }
|
||||
];
|
||||
|
||||
const messages = [];
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const sizeType = messageSizes[i % messageSizes.length];
|
||||
messages.push(new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i + 1}@example.com`],
|
||||
subject: `Variable size test ${i + 1} (${sizeType.size})`,
|
||||
text: sizeType.content
|
||||
}));
|
||||
}
|
||||
|
||||
console.log(` Sending ${messages.length} messages of varying sizes...`);
|
||||
const variableStart = Date.now();
|
||||
|
||||
const results = await Promise.all(
|
||||
messages.map(message => smtpClient.sendMail(message))
|
||||
);
|
||||
|
||||
const variableTime = Date.now() - variableStart;
|
||||
const variableRate = (messages.length / variableTime) * 1000;
|
||||
const bytesPerSecond = (totalBytes / variableTime) * 1000;
|
||||
|
||||
console.log(` Variable size throughput: ${variableRate.toFixed(2)} messages/second`);
|
||||
console.log(` Data throughput: ${(bytesPerSecond / 1024).toFixed(2)} KB/second`);
|
||||
console.log(` Average message size: ${Math.round(totalBytes / messages.length)} bytes`);
|
||||
|
||||
expect(variableRate).toBeGreaterThan(2);
|
||||
expect(results.length).toBe(messages.length);
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 6: Sustained throughput over time
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing sustained throughput over time`);
|
||||
|
||||
let messageCount = 0;
|
||||
const timestamps: number[] = [];
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 sustained.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-sustained.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
messageCount++;
|
||||
timestamps.push(Date.now());
|
||||
socket.write(`250 OK: Sustained message ${messageCount}\r\n`);
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 3
|
||||
});
|
||||
|
||||
const totalMessages = 30;
|
||||
const batchSize = 5;
|
||||
const batchDelay = 500; // 500ms between batches
|
||||
|
||||
console.log(` Sending ${totalMessages} messages in batches of ${batchSize}...`);
|
||||
const sustainedStart = Date.now();
|
||||
|
||||
for (let batch = 0; batch < totalMessages / batchSize; batch++) {
|
||||
const batchMessages = Array(batchSize).fill(null).map((_, i) => {
|
||||
const msgIndex = batch * batchSize + i + 1;
|
||||
return new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${msgIndex}@example.com`],
|
||||
subject: `Sustained test batch ${batch + 1} message ${i + 1}`,
|
||||
text: `Testing sustained throughput - message ${msgIndex}`
|
||||
});
|
||||
});
|
||||
|
||||
// Send batch concurrently
|
||||
await Promise.all(
|
||||
batchMessages.map(message => smtpClient.sendMail(message))
|
||||
);
|
||||
|
||||
console.log(` Batch ${batch + 1} completed (${(batch + 1) * batchSize} messages total)`);
|
||||
|
||||
// Delay between batches (except last)
|
||||
if (batch < (totalMessages / batchSize) - 1) {
|
||||
await new Promise(resolve => setTimeout(resolve, batchDelay));
|
||||
}
|
||||
}
|
||||
|
||||
const sustainedTime = Date.now() - sustainedStart;
|
||||
const sustainedRate = (totalMessages / sustainedTime) * 1000;
|
||||
|
||||
// Calculate rate stability
|
||||
const windowSize = 5;
|
||||
const rates: number[] = [];
|
||||
for (let i = windowSize; i < timestamps.length; i++) {
|
||||
const windowStart = timestamps[i - windowSize];
|
||||
const windowEnd = timestamps[i];
|
||||
const windowRate = (windowSize / (windowEnd - windowStart)) * 1000;
|
||||
rates.push(windowRate);
|
||||
}
|
||||
|
||||
const avgRate = rates.reduce((a, b) => a + b, 0) / rates.length;
|
||||
const rateVariance = rates.reduce((acc, rate) => acc + Math.pow(rate - avgRate, 2), 0) / rates.length;
|
||||
const rateStdDev = Math.sqrt(rateVariance);
|
||||
|
||||
console.log(` Sustained throughput: ${sustainedRate.toFixed(2)} messages/second`);
|
||||
console.log(` Average windowed rate: ${avgRate.toFixed(2)} ± ${rateStdDev.toFixed(2)} msg/sec`);
|
||||
console.log(` Rate stability: ${((1 - rateStdDev / avgRate) * 100).toFixed(1)}%`);
|
||||
|
||||
expect(sustainedRate).toBeGreaterThan(3);
|
||||
expect(rateStdDev / avgRate).toBeLessThan(0.5); // Coefficient of variation < 50%
|
||||
|
||||
await smtpClient.close();
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
console.log(`\n${testId}: All ${scenarioCount} message throughput scenarios tested ✓`);
|
||||
});
|
641
test/suite/smtpclient_performance/test.cperf-03.memory-usage.ts
Normal file
641
test/suite/smtpclient_performance/test.cperf-03.memory-usage.ts
Normal file
@ -0,0 +1,641 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as plugins from './plugins.js';
|
||||
import { createTestServer } from '../../helpers/server.loader.js';
|
||||
import { createSmtpClient } from '../../helpers/smtp.client.js';
|
||||
|
||||
tap.test('CPERF-03: should optimize memory usage', async (tools) => {
|
||||
const testId = 'CPERF-03-memory-usage';
|
||||
console.log(`\n${testId}: Testing memory usage optimization...`);
|
||||
|
||||
let scenarioCount = 0;
|
||||
|
||||
// Helper function to get memory usage
|
||||
const getMemoryUsage = () => {
|
||||
if (process.memoryUsage) {
|
||||
const usage = process.memoryUsage();
|
||||
return {
|
||||
heapUsed: usage.heapUsed,
|
||||
heapTotal: usage.heapTotal,
|
||||
external: usage.external,
|
||||
rss: usage.rss
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Helper function to format bytes
|
||||
const formatBytes = (bytes: number) => {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
};
|
||||
|
||||
// Scenario 1: Memory usage during connection lifecycle
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing memory usage during connection lifecycle`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 memory.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-memory.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Force garbage collection if available
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
}
|
||||
|
||||
const beforeConnection = getMemoryUsage();
|
||||
console.log(` Memory before connection: ${formatBytes(beforeConnection?.heapUsed || 0)}`);
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
const afterConnection = getMemoryUsage();
|
||||
console.log(` Memory after client creation: ${formatBytes(afterConnection?.heapUsed || 0)}`);
|
||||
|
||||
// Send a test email
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Memory usage test',
|
||||
text: 'Testing memory usage during email sending'
|
||||
});
|
||||
|
||||
await smtpClient.sendMail(email);
|
||||
|
||||
const afterSending = getMemoryUsage();
|
||||
console.log(` Memory after sending: ${formatBytes(afterSending?.heapUsed || 0)}`);
|
||||
|
||||
// Close connection
|
||||
if (smtpClient.close) {
|
||||
await smtpClient.close();
|
||||
}
|
||||
|
||||
// Force garbage collection if available
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
const afterClose = getMemoryUsage();
|
||||
console.log(` Memory after close: ${formatBytes(afterClose?.heapUsed || 0)}`);
|
||||
|
||||
// Check for memory leaks
|
||||
const memoryIncrease = (afterClose?.heapUsed || 0) - (beforeConnection?.heapUsed || 0);
|
||||
console.log(` Net memory change: ${formatBytes(Math.abs(memoryIncrease))} ${memoryIncrease >= 0 ? 'increase' : 'decrease'}`);
|
||||
|
||||
// Memory increase should be minimal after cleanup
|
||||
expect(memoryIncrease).toBeLessThan(1024 * 1024); // Less than 1MB increase
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 2: Memory usage with large messages
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing memory usage with large messages`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 large.example.com ESMTP\r\n');
|
||||
|
||||
let inData = false;
|
||||
let messageSize = 0;
|
||||
|
||||
socket.on('data', (data) => {
|
||||
if (inData) {
|
||||
messageSize += data.length;
|
||||
if (data.toString().includes('\r\n.\r\n')) {
|
||||
inData = false;
|
||||
console.log(` [Server] Received message: ${formatBytes(messageSize)}`);
|
||||
socket.write('250 OK\r\n');
|
||||
messageSize = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-large.example.com\r\n');
|
||||
socket.write('250-SIZE 52428800\r\n'); // 50MB limit
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
inData = true;
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
}
|
||||
|
||||
const beforeLarge = getMemoryUsage();
|
||||
console.log(` Memory before large message: ${formatBytes(beforeLarge?.heapUsed || 0)}`);
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Create messages of increasing sizes
|
||||
const messageSizes = [
|
||||
{ name: '1KB', size: 1024 },
|
||||
{ name: '10KB', size: 10 * 1024 },
|
||||
{ name: '100KB', size: 100 * 1024 },
|
||||
{ name: '1MB', size: 1024 * 1024 },
|
||||
{ name: '5MB', size: 5 * 1024 * 1024 }
|
||||
];
|
||||
|
||||
for (const msgSize of messageSizes) {
|
||||
console.log(` Testing ${msgSize.name} message...`);
|
||||
|
||||
const beforeMessage = getMemoryUsage();
|
||||
|
||||
// Create large content
|
||||
const largeContent = 'x'.repeat(msgSize.size);
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: `Large message test - ${msgSize.name}`,
|
||||
text: largeContent
|
||||
});
|
||||
|
||||
const duringCreation = getMemoryUsage();
|
||||
const creationIncrease = (duringCreation?.heapUsed || 0) - (beforeMessage?.heapUsed || 0);
|
||||
console.log(` Memory increase during creation: ${formatBytes(creationIncrease)}`);
|
||||
|
||||
await smtpClient.sendMail(email);
|
||||
|
||||
const afterSending = getMemoryUsage();
|
||||
const sendingIncrease = (afterSending?.heapUsed || 0) - (beforeMessage?.heapUsed || 0);
|
||||
console.log(` Memory increase after sending: ${formatBytes(sendingIncrease)}`);
|
||||
|
||||
// Clear reference to email
|
||||
// email = null; // Can't reassign const
|
||||
|
||||
// Memory usage shouldn't grow linearly with message size
|
||||
// due to streaming or buffering optimizations
|
||||
expect(sendingIncrease).toBeLessThan(msgSize.size * 2); // At most 2x the message size
|
||||
}
|
||||
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
const afterLarge = getMemoryUsage();
|
||||
console.log(` Memory after large messages: ${formatBytes(afterLarge?.heapUsed || 0)}`);
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 3: Memory usage with multiple concurrent connections
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing memory usage with concurrent connections`);
|
||||
|
||||
let connectionCount = 0;
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
connectionCount++;
|
||||
const connId = connectionCount;
|
||||
console.log(` [Server] Connection ${connId} established`);
|
||||
socket.write('220 concurrent.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('close', () => {
|
||||
console.log(` [Server] Connection ${connId} closed`);
|
||||
});
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-concurrent.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
}
|
||||
|
||||
const beforeConcurrent = getMemoryUsage();
|
||||
console.log(` Memory before concurrent connections: ${formatBytes(beforeConcurrent?.heapUsed || 0)}`);
|
||||
|
||||
const concurrentCount = 10;
|
||||
const clients: any[] = [];
|
||||
|
||||
// Create multiple concurrent clients
|
||||
for (let i = 0; i < concurrentCount; i++) {
|
||||
const client = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
clients.push(client);
|
||||
}
|
||||
|
||||
const afterCreation = getMemoryUsage();
|
||||
const creationIncrease = (afterCreation?.heapUsed || 0) - (beforeConcurrent?.heapUsed || 0);
|
||||
console.log(` Memory after creating ${concurrentCount} clients: ${formatBytes(creationIncrease)}`);
|
||||
|
||||
// Send emails concurrently
|
||||
const promises = clients.map((client, i) => {
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i + 1}@example.com`],
|
||||
subject: `Concurrent memory test ${i + 1}`,
|
||||
text: `Testing concurrent memory usage - client ${i + 1}`
|
||||
});
|
||||
return client.sendMail(email);
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
const afterSending = getMemoryUsage();
|
||||
const sendingIncrease = (afterSending?.heapUsed || 0) - (beforeConcurrent?.heapUsed || 0);
|
||||
console.log(` Memory after concurrent sending: ${formatBytes(sendingIncrease)}`);
|
||||
|
||||
// Close all clients
|
||||
await Promise.all(clients.map(client => {
|
||||
if (client.close) {
|
||||
return client.close();
|
||||
}
|
||||
return Promise.resolve();
|
||||
}));
|
||||
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
const afterClose = getMemoryUsage();
|
||||
const finalIncrease = (afterClose?.heapUsed || 0) - (beforeConcurrent?.heapUsed || 0);
|
||||
console.log(` Memory after closing all connections: ${formatBytes(finalIncrease)}`);
|
||||
|
||||
// Memory per connection should be reasonable
|
||||
const memoryPerConnection = creationIncrease / concurrentCount;
|
||||
console.log(` Average memory per connection: ${formatBytes(memoryPerConnection)}`);
|
||||
|
||||
expect(memoryPerConnection).toBeLessThan(512 * 1024); // Less than 512KB per connection
|
||||
expect(finalIncrease).toBeLessThan(creationIncrease * 0.5); // Significant cleanup
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 4: Memory usage with connection pooling
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing memory usage with connection pooling`);
|
||||
|
||||
let connectionCount = 0;
|
||||
let maxConnections = 0;
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
connectionCount++;
|
||||
maxConnections = Math.max(maxConnections, connectionCount);
|
||||
console.log(` [Server] Connection established (total: ${connectionCount}, max: ${maxConnections})`);
|
||||
socket.write('220 pool.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('close', () => {
|
||||
connectionCount--;
|
||||
});
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-pool.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
}
|
||||
|
||||
const beforePool = getMemoryUsage();
|
||||
console.log(` Memory before pooling: ${formatBytes(beforePool?.heapUsed || 0)}`);
|
||||
|
||||
const pooledClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 5,
|
||||
maxMessages: 100
|
||||
});
|
||||
|
||||
const afterPoolCreation = getMemoryUsage();
|
||||
const poolCreationIncrease = (afterPoolCreation?.heapUsed || 0) - (beforePool?.heapUsed || 0);
|
||||
console.log(` Memory after pool creation: ${formatBytes(poolCreationIncrease)}`);
|
||||
|
||||
// Send many emails through the pool
|
||||
const emailCount = 30;
|
||||
const emails = Array(emailCount).fill(null).map((_, i) =>
|
||||
new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i + 1}@example.com`],
|
||||
subject: `Pooled memory test ${i + 1}`,
|
||||
text: `Testing pooled memory usage - email ${i + 1}`
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(emails.map(email => pooledClient.sendMail(email)));
|
||||
|
||||
const afterPoolSending = getMemoryUsage();
|
||||
const poolSendingIncrease = (afterPoolSending?.heapUsed || 0) - (beforePool?.heapUsed || 0);
|
||||
console.log(` Memory after sending ${emailCount} emails: ${formatBytes(poolSendingIncrease)}`);
|
||||
console.log(` Maximum concurrent connections: ${maxConnections}`);
|
||||
|
||||
await pooledClient.close();
|
||||
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
const afterPoolClose = getMemoryUsage();
|
||||
const poolFinalIncrease = (afterPoolClose?.heapUsed || 0) - (beforePool?.heapUsed || 0);
|
||||
console.log(` Memory after pool close: ${formatBytes(poolFinalIncrease)}`);
|
||||
|
||||
// Pooling should use fewer connections and thus less memory
|
||||
expect(maxConnections).toBeLessThanOrEqual(5);
|
||||
expect(poolFinalIncrease).toBeLessThan(2 * 1024 * 1024); // Less than 2MB final increase
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 5: Memory usage with attachments
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing memory usage with attachments`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 attachments.example.com ESMTP\r\n');
|
||||
|
||||
let inData = false;
|
||||
let totalSize = 0;
|
||||
|
||||
socket.on('data', (data) => {
|
||||
if (inData) {
|
||||
totalSize += data.length;
|
||||
if (data.toString().includes('\r\n.\r\n')) {
|
||||
inData = false;
|
||||
console.log(` [Server] Received email with attachments: ${formatBytes(totalSize)}`);
|
||||
socket.write('250 OK\r\n');
|
||||
totalSize = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-attachments.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
inData = true;
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
}
|
||||
|
||||
const beforeAttachments = getMemoryUsage();
|
||||
console.log(` Memory before attachments: ${formatBytes(beforeAttachments?.heapUsed || 0)}`);
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Test with different attachment sizes
|
||||
const attachmentSizes = [
|
||||
{ name: 'small', size: 10 * 1024 }, // 10KB
|
||||
{ name: 'medium', size: 100 * 1024 }, // 100KB
|
||||
{ name: 'large', size: 1024 * 1024 } // 1MB
|
||||
];
|
||||
|
||||
for (const attachSize of attachmentSizes) {
|
||||
console.log(` Testing ${attachSize.name} attachment (${formatBytes(attachSize.size)})...`);
|
||||
|
||||
const beforeAttachment = getMemoryUsage();
|
||||
|
||||
// Create binary attachment data
|
||||
const attachmentData = Buffer.alloc(attachSize.size);
|
||||
for (let i = 0; i < attachmentData.length; i++) {
|
||||
attachmentData[i] = i % 256;
|
||||
}
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: `Attachment memory test - ${attachSize.name}`,
|
||||
text: `Testing memory usage with ${attachSize.name} attachment`,
|
||||
attachments: [{
|
||||
filename: `${attachSize.name}-file.bin`,
|
||||
content: attachmentData
|
||||
}]
|
||||
});
|
||||
|
||||
const afterCreation = getMemoryUsage();
|
||||
const creationIncrease = (afterCreation?.heapUsed || 0) - (beforeAttachment?.heapUsed || 0);
|
||||
console.log(` Memory increase during email creation: ${formatBytes(creationIncrease)}`);
|
||||
|
||||
await smtpClient.sendMail(email);
|
||||
|
||||
const afterSending = getMemoryUsage();
|
||||
const sendingIncrease = (afterSending?.heapUsed || 0) - (beforeAttachment?.heapUsed || 0);
|
||||
console.log(` Memory increase after sending: ${formatBytes(sendingIncrease)}`);
|
||||
|
||||
// Memory usage should be efficient (not holding multiple copies)
|
||||
expect(creationIncrease).toBeLessThan(attachSize.size * 3); // At most 3x (original + base64 + overhead)
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 6: Memory leak detection
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing for memory leaks`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
socket.write('220 leak-test.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-leak-test.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Perform multiple iterations to detect leaks
|
||||
const iterations = 5;
|
||||
const memoryMeasurements: number[] = [];
|
||||
|
||||
for (let iteration = 0; iteration < iterations; iteration++) {
|
||||
console.log(` Iteration ${iteration + 1}/${iterations}...`);
|
||||
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
const beforeIteration = getMemoryUsage();
|
||||
|
||||
// Create and use SMTP client
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Send multiple emails
|
||||
const emails = Array(10).fill(null).map((_, i) =>
|
||||
new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i + 1}@example.com`],
|
||||
subject: `Leak test iteration ${iteration + 1} email ${i + 1}`,
|
||||
text: `Testing for memory leaks - iteration ${iteration + 1}, email ${i + 1}`
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(emails.map(email => smtpClient.sendMail(email)));
|
||||
|
||||
// Close client
|
||||
if (smtpClient.close) {
|
||||
await smtpClient.close();
|
||||
}
|
||||
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
const afterIteration = getMemoryUsage();
|
||||
const iterationIncrease = (afterIteration?.heapUsed || 0) - (beforeIteration?.heapUsed || 0);
|
||||
memoryMeasurements.push(iterationIncrease);
|
||||
|
||||
console.log(` Memory change: ${formatBytes(iterationIncrease)}`);
|
||||
}
|
||||
|
||||
// Analyze memory trend
|
||||
const avgIncrease = memoryMeasurements.reduce((a, b) => a + b, 0) / memoryMeasurements.length;
|
||||
const maxIncrease = Math.max(...memoryMeasurements);
|
||||
const minIncrease = Math.min(...memoryMeasurements);
|
||||
|
||||
console.log(` Memory leak analysis:`);
|
||||
console.log(` Average increase: ${formatBytes(avgIncrease)}`);
|
||||
console.log(` Min increase: ${formatBytes(minIncrease)}`);
|
||||
console.log(` Max increase: ${formatBytes(maxIncrease)}`);
|
||||
console.log(` Range: ${formatBytes(maxIncrease - minIncrease)}`);
|
||||
|
||||
// Check for significant memory leaks
|
||||
// Memory should not consistently increase across iterations
|
||||
expect(avgIncrease).toBeLessThan(512 * 1024); // Less than 512KB average increase
|
||||
expect(maxIncrease - minIncrease).toBeLessThan(1024 * 1024); // Range less than 1MB
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
console.log(`\n${testId}: All ${scenarioCount} memory usage scenarios tested ✓`);
|
||||
});
|
@ -0,0 +1,670 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as plugins from './plugins.js';
|
||||
import { createTestServer } from '../../helpers/server.loader.js';
|
||||
import { createSmtpClient } from '../../helpers/smtp.client.js';
|
||||
|
||||
tap.test('CPERF-04: should optimize CPU utilization', async (tools) => {
|
||||
const testId = 'CPERF-04-cpu-utilization';
|
||||
console.log(`\n${testId}: Testing CPU utilization optimization...`);
|
||||
|
||||
let scenarioCount = 0;
|
||||
|
||||
// Helper function to measure CPU usage (simplified)
|
||||
const measureCpuUsage = async (duration: number) => {
|
||||
const start = process.cpuUsage();
|
||||
const startTime = Date.now();
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, duration));
|
||||
|
||||
const end = process.cpuUsage(start);
|
||||
const elapsed = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
user: end.user / 1000, // Convert to milliseconds
|
||||
system: end.system / 1000,
|
||||
total: (end.user + end.system) / 1000,
|
||||
elapsed,
|
||||
userPercent: (end.user / 1000) / elapsed * 100,
|
||||
systemPercent: (end.system / 1000) / elapsed * 100,
|
||||
totalPercent: ((end.user + end.system) / 1000) / elapsed * 100
|
||||
};
|
||||
};
|
||||
|
||||
// Scenario 1: CPU usage during connection establishment
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing CPU usage during connection establishment`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 cpu.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-cpu.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Measure CPU during multiple connection establishments
|
||||
const connectionCount = 10;
|
||||
console.log(` Establishing ${connectionCount} connections...`);
|
||||
|
||||
const startCpu = process.cpuUsage();
|
||||
const startTime = Date.now();
|
||||
|
||||
const clients: any[] = [];
|
||||
for (let i = 0; i < connectionCount; i++) {
|
||||
const client = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
clients.push(client);
|
||||
|
||||
// Verify connection
|
||||
try {
|
||||
await client.verify();
|
||||
} catch (error) {
|
||||
// Connection verification might not be available
|
||||
}
|
||||
}
|
||||
|
||||
const endCpu = process.cpuUsage(startCpu);
|
||||
const elapsed = Date.now() - startTime;
|
||||
|
||||
const cpuUsage = {
|
||||
user: endCpu.user / 1000,
|
||||
system: endCpu.system / 1000,
|
||||
total: (endCpu.user + endCpu.system) / 1000,
|
||||
userPercent: (endCpu.user / 1000) / elapsed * 100,
|
||||
systemPercent: (endCpu.system / 1000) / elapsed * 100,
|
||||
totalPercent: ((endCpu.user + endCpu.system) / 1000) / elapsed * 100
|
||||
};
|
||||
|
||||
console.log(` Connection establishment CPU usage:`);
|
||||
console.log(` Total time: ${elapsed}ms`);
|
||||
console.log(` User CPU: ${cpuUsage.user.toFixed(1)}ms (${cpuUsage.userPercent.toFixed(1)}%)`);
|
||||
console.log(` System CPU: ${cpuUsage.system.toFixed(1)}ms (${cpuUsage.systemPercent.toFixed(1)}%)`);
|
||||
console.log(` Total CPU: ${cpuUsage.total.toFixed(1)}ms (${cpuUsage.totalPercent.toFixed(1)}%)`);
|
||||
console.log(` CPU per connection: ${(cpuUsage.total / connectionCount).toFixed(1)}ms`);
|
||||
|
||||
// Close all connections
|
||||
await Promise.all(clients.map(client => {
|
||||
if (client.close) {
|
||||
return client.close();
|
||||
}
|
||||
return Promise.resolve();
|
||||
}));
|
||||
|
||||
// CPU usage should be reasonable
|
||||
expect(cpuUsage.totalPercent).toBeLessThan(50); // Less than 50% CPU usage
|
||||
expect(cpuUsage.total / connectionCount).toBeLessThan(50); // Less than 50ms CPU per connection
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 2: CPU usage during message composition
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing CPU usage during message composition`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
socket.write('220 composition.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-composition.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Test different message compositions
|
||||
const compositionTests = [
|
||||
{
|
||||
name: 'Simple text',
|
||||
email: new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Simple text message',
|
||||
text: 'This is a simple text message.'
|
||||
})
|
||||
},
|
||||
{
|
||||
name: 'HTML with formatting',
|
||||
email: new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'HTML message',
|
||||
html: '<h1>HTML Message</h1><p>This is an <strong>HTML</strong> message with <em>formatting</em>.</p>'
|
||||
})
|
||||
},
|
||||
{
|
||||
name: 'Multipart with text and HTML',
|
||||
email: new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Multipart message',
|
||||
text: 'Plain text version',
|
||||
html: '<p>HTML version</p>'
|
||||
})
|
||||
},
|
||||
{
|
||||
name: 'Message with small attachment',
|
||||
email: new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Message with attachment',
|
||||
text: 'Message with small attachment',
|
||||
attachments: [{
|
||||
filename: 'test.txt',
|
||||
content: 'This is a test attachment with some content.'
|
||||
}]
|
||||
})
|
||||
}
|
||||
];
|
||||
|
||||
for (const test of compositionTests) {
|
||||
console.log(` Testing ${test.name}...`);
|
||||
|
||||
const startCpu = process.cpuUsage();
|
||||
const startTime = Date.now();
|
||||
|
||||
await smtpClient.sendMail(test.email);
|
||||
|
||||
const endCpu = process.cpuUsage(startCpu);
|
||||
const elapsed = Date.now() - startTime;
|
||||
|
||||
const cpuUsage = {
|
||||
user: endCpu.user / 1000,
|
||||
system: endCpu.system / 1000,
|
||||
total: (endCpu.user + endCpu.system) / 1000,
|
||||
totalPercent: ((endCpu.user + endCpu.system) / 1000) / elapsed * 100
|
||||
};
|
||||
|
||||
console.log(` ${test.name}: ${cpuUsage.total.toFixed(1)}ms CPU (${cpuUsage.totalPercent.toFixed(1)}%)`);
|
||||
|
||||
// CPU usage should be efficient for message composition
|
||||
expect(cpuUsage.totalPercent).toBeLessThan(25); // Less than 25% CPU
|
||||
expect(cpuUsage.total).toBeLessThan(100); // Less than 100ms CPU time
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 3: CPU usage with concurrent operations
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing CPU usage with concurrent operations`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
socket.write('220 concurrent.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-concurrent.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Test sequential vs concurrent CPU usage
|
||||
const messageCount = 20;
|
||||
const emails = Array(messageCount).fill(null).map((_, i) =>
|
||||
new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i + 1}@example.com`],
|
||||
subject: `CPU test message ${i + 1}`,
|
||||
text: `Testing CPU utilization - message ${i + 1}`
|
||||
})
|
||||
);
|
||||
|
||||
// Sequential sending
|
||||
console.log(` Sequential sending of ${messageCount} messages...`);
|
||||
const sequentialStartCpu = process.cpuUsage();
|
||||
const sequentialStartTime = Date.now();
|
||||
|
||||
for (const email of emails) {
|
||||
await smtpClient.sendMail(email);
|
||||
}
|
||||
|
||||
const sequentialEndCpu = process.cpuUsage(sequentialStartCpu);
|
||||
const sequentialElapsed = Date.now() - sequentialStartTime;
|
||||
|
||||
const sequentialCpu = {
|
||||
total: (sequentialEndCpu.user + sequentialEndCpu.system) / 1000,
|
||||
totalPercent: ((sequentialEndCpu.user + sequentialEndCpu.system) / 1000) / sequentialElapsed * 100
|
||||
};
|
||||
|
||||
console.log(` Sequential: ${sequentialCpu.total.toFixed(1)}ms CPU (${sequentialCpu.totalPercent.toFixed(1)}%)`);
|
||||
console.log(` Per message: ${(sequentialCpu.total / messageCount).toFixed(1)}ms CPU`);
|
||||
|
||||
// Concurrent sending (new emails)
|
||||
const concurrentEmails = Array(messageCount).fill(null).map((_, i) =>
|
||||
new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`concurrent${i + 1}@example.com`],
|
||||
subject: `Concurrent CPU test ${i + 1}`,
|
||||
text: `Testing concurrent CPU utilization - message ${i + 1}`
|
||||
})
|
||||
);
|
||||
|
||||
console.log(` Concurrent sending of ${messageCount} messages...`);
|
||||
const concurrentStartCpu = process.cpuUsage();
|
||||
const concurrentStartTime = Date.now();
|
||||
|
||||
await Promise.all(concurrentEmails.map(email => smtpClient.sendMail(email)));
|
||||
|
||||
const concurrentEndCpu = process.cpuUsage(concurrentStartCpu);
|
||||
const concurrentElapsed = Date.now() - concurrentStartTime;
|
||||
|
||||
const concurrentCpu = {
|
||||
total: (concurrentEndCpu.user + concurrentEndCpu.system) / 1000,
|
||||
totalPercent: ((concurrentEndCpu.user + concurrentEndCpu.system) / 1000) / concurrentElapsed * 100
|
||||
};
|
||||
|
||||
console.log(` Concurrent: ${concurrentCpu.total.toFixed(1)}ms CPU (${concurrentCpu.totalPercent.toFixed(1)}%)`);
|
||||
console.log(` Per message: ${(concurrentCpu.total / messageCount).toFixed(1)}ms CPU`);
|
||||
|
||||
// Compare efficiency
|
||||
const efficiency = sequentialCpu.total / concurrentCpu.total;
|
||||
console.log(` CPU efficiency ratio: ${efficiency.toFixed(2)}x`);
|
||||
|
||||
// Concurrent should be more CPU efficient (higher throughput)
|
||||
expect(concurrentElapsed).toBeLessThan(sequentialElapsed);
|
||||
expect(concurrentCpu.totalPercent).toBeLessThan(80); // Less than 80% CPU
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 4: CPU usage with large attachments
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing CPU usage with large attachments`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
socket.write('220 attachments.example.com ESMTP\r\n');
|
||||
|
||||
let inData = false;
|
||||
let dataSize = 0;
|
||||
|
||||
socket.on('data', (data) => {
|
||||
if (inData) {
|
||||
dataSize += data.length;
|
||||
if (data.toString().includes('\r\n.\r\n')) {
|
||||
inData = false;
|
||||
console.log(` [Server] Received ${(dataSize / 1024).toFixed(1)}KB`);
|
||||
socket.write('250 OK\r\n');
|
||||
dataSize = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-attachments.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
inData = true;
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Test different attachment sizes
|
||||
const attachmentSizes = [
|
||||
{ name: '10KB', size: 10 * 1024 },
|
||||
{ name: '100KB', size: 100 * 1024 },
|
||||
{ name: '1MB', size: 1024 * 1024 }
|
||||
];
|
||||
|
||||
for (const attachSize of attachmentSizes) {
|
||||
console.log(` Testing ${attachSize.name} attachment...`);
|
||||
|
||||
// Create binary attachment
|
||||
const attachmentData = Buffer.alloc(attachSize.size);
|
||||
for (let i = 0; i < attachmentData.length; i++) {
|
||||
attachmentData[i] = i % 256;
|
||||
}
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: `CPU test with ${attachSize.name} attachment`,
|
||||
text: `Testing CPU usage with ${attachSize.name} attachment`,
|
||||
attachments: [{
|
||||
filename: `${attachSize.name}-file.bin`,
|
||||
content: attachmentData
|
||||
}]
|
||||
});
|
||||
|
||||
const startCpu = process.cpuUsage();
|
||||
const startTime = Date.now();
|
||||
|
||||
await smtpClient.sendMail(email);
|
||||
|
||||
const endCpu = process.cpuUsage(startCpu);
|
||||
const elapsed = Date.now() - startTime;
|
||||
|
||||
const cpuUsage = {
|
||||
total: (endCpu.user + endCpu.system) / 1000,
|
||||
totalPercent: ((endCpu.user + endCpu.system) / 1000) / elapsed * 100
|
||||
};
|
||||
|
||||
const cpuPerKB = cpuUsage.total / (attachSize.size / 1024);
|
||||
|
||||
console.log(` ${attachSize.name}: ${cpuUsage.total.toFixed(1)}ms CPU (${cpuUsage.totalPercent.toFixed(1)}%)`);
|
||||
console.log(` CPU per KB: ${cpuPerKB.toFixed(3)}ms/KB`);
|
||||
|
||||
// CPU usage should scale reasonably with attachment size
|
||||
expect(cpuUsage.totalPercent).toBeLessThan(50);
|
||||
expect(cpuPerKB).toBeLessThan(1); // Less than 1ms CPU per KB
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 5: CPU usage with connection pooling
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing CPU usage with connection pooling`);
|
||||
|
||||
let connectionCount = 0;
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
connectionCount++;
|
||||
socket.write('220 pool.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-pool.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Compare individual connections vs pooled
|
||||
const messageCount = 15;
|
||||
|
||||
// Individual connections
|
||||
console.log(` Testing ${messageCount} individual connections...`);
|
||||
connectionCount = 0;
|
||||
const individualStartCpu = process.cpuUsage();
|
||||
const individualStartTime = Date.now();
|
||||
|
||||
for (let i = 0; i < messageCount; i++) {
|
||||
const client = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`individual${i + 1}@example.com`],
|
||||
subject: `Individual connection test ${i + 1}`,
|
||||
text: `Testing individual connection - message ${i + 1}`
|
||||
});
|
||||
|
||||
await client.sendMail(email);
|
||||
|
||||
if (client.close) {
|
||||
await client.close();
|
||||
}
|
||||
}
|
||||
|
||||
const individualEndCpu = process.cpuUsage(individualStartCpu);
|
||||
const individualElapsed = Date.now() - individualStartTime;
|
||||
const individualConnections = connectionCount;
|
||||
|
||||
const individualCpu = {
|
||||
total: (individualEndCpu.user + individualEndCpu.system) / 1000,
|
||||
totalPercent: ((individualEndCpu.user + individualEndCpu.system) / 1000) / individualElapsed * 100
|
||||
};
|
||||
|
||||
console.log(` Individual: ${individualCpu.total.toFixed(1)}ms CPU, ${individualConnections} connections`);
|
||||
|
||||
// Pooled connections
|
||||
console.log(` Testing pooled connections...`);
|
||||
connectionCount = 0;
|
||||
const pooledStartCpu = process.cpuUsage();
|
||||
const pooledStartTime = Date.now();
|
||||
|
||||
const pooledClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 3,
|
||||
maxMessages: 100
|
||||
});
|
||||
|
||||
const pooledEmails = Array(messageCount).fill(null).map((_, i) =>
|
||||
new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`pooled${i + 1}@example.com`],
|
||||
subject: `Pooled connection test ${i + 1}`,
|
||||
text: `Testing pooled connection - message ${i + 1}`
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(pooledEmails.map(email => pooledClient.sendMail(email)));
|
||||
await pooledClient.close();
|
||||
|
||||
const pooledEndCpu = process.cpuUsage(pooledStartCpu);
|
||||
const pooledElapsed = Date.now() - pooledStartTime;
|
||||
const pooledConnections = connectionCount;
|
||||
|
||||
const pooledCpu = {
|
||||
total: (pooledEndCpu.user + pooledEndCpu.system) / 1000,
|
||||
totalPercent: ((pooledEndCpu.user + pooledEndCpu.system) / 1000) / pooledElapsed * 100
|
||||
};
|
||||
|
||||
console.log(` Pooled: ${pooledCpu.total.toFixed(1)}ms CPU, ${pooledConnections} connections`);
|
||||
|
||||
const cpuEfficiency = individualCpu.total / pooledCpu.total;
|
||||
const connectionEfficiency = individualConnections / pooledConnections;
|
||||
|
||||
console.log(` CPU efficiency: ${cpuEfficiency.toFixed(2)}x`);
|
||||
console.log(` Connection efficiency: ${connectionEfficiency.toFixed(2)}x`);
|
||||
|
||||
// Pooling should be more CPU efficient
|
||||
expect(pooledCpu.total).toBeLessThan(individualCpu.total);
|
||||
expect(pooledConnections).toBeLessThan(individualConnections);
|
||||
expect(cpuEfficiency).toBeGreaterThan(1.2); // At least 20% more efficient
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 6: CPU usage under stress
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing CPU usage under stress`);
|
||||
|
||||
let messageCount = 0;
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
socket.write('220 stress.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-stress.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
messageCount++;
|
||||
socket.write(`250 OK: Message ${messageCount}\r\n`);
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const pooledClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 5,
|
||||
maxMessages: 100
|
||||
});
|
||||
|
||||
// Stress test with many messages
|
||||
const stressMessageCount = 50;
|
||||
const stressEmails = Array(stressMessageCount).fill(null).map((_, i) =>
|
||||
new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`stress${i + 1}@example.com`],
|
||||
subject: `Stress test message ${i + 1}`,
|
||||
text: `Testing CPU under stress - message ${i + 1}`
|
||||
})
|
||||
);
|
||||
|
||||
console.log(` Stress testing with ${stressMessageCount} concurrent messages...`);
|
||||
|
||||
const stressStartCpu = process.cpuUsage();
|
||||
const stressStartTime = Date.now();
|
||||
|
||||
// Monitor CPU usage during stress test
|
||||
const cpuSamples: number[] = [];
|
||||
const sampleInterval = setInterval(() => {
|
||||
const currentCpu = process.cpuUsage(stressStartCpu);
|
||||
const currentElapsed = Date.now() - stressStartTime;
|
||||
const currentPercent = ((currentCpu.user + currentCpu.system) / 1000) / currentElapsed * 100;
|
||||
cpuSamples.push(currentPercent);
|
||||
}, 100);
|
||||
|
||||
await Promise.all(stressEmails.map(email => pooledClient.sendMail(email)));
|
||||
|
||||
clearInterval(sampleInterval);
|
||||
|
||||
const stressEndCpu = process.cpuUsage(stressStartCpu);
|
||||
const stressElapsed = Date.now() - stressStartTime;
|
||||
|
||||
const stressCpu = {
|
||||
total: (stressEndCpu.user + stressEndCpu.system) / 1000,
|
||||
totalPercent: ((stressEndCpu.user + stressEndCpu.system) / 1000) / stressElapsed * 100
|
||||
};
|
||||
|
||||
const maxCpuSample = Math.max(...cpuSamples);
|
||||
const avgCpuSample = cpuSamples.reduce((a, b) => a + b, 0) / cpuSamples.length;
|
||||
|
||||
console.log(` Stress test results:`);
|
||||
console.log(` Total CPU: ${stressCpu.total.toFixed(1)}ms (${stressCpu.totalPercent.toFixed(1)}%)`);
|
||||
console.log(` Peak CPU: ${maxCpuSample.toFixed(1)}%`);
|
||||
console.log(` Average CPU: ${avgCpuSample.toFixed(1)}%`);
|
||||
console.log(` Messages per CPU ms: ${(stressMessageCount / stressCpu.total).toFixed(2)}`);
|
||||
console.log(` Throughput: ${(stressMessageCount / stressElapsed * 1000).toFixed(1)} msg/sec`);
|
||||
|
||||
await pooledClient.close();
|
||||
|
||||
// CPU usage should remain reasonable under stress
|
||||
expect(stressCpu.totalPercent).toBeLessThan(90); // Less than 90% CPU
|
||||
expect(maxCpuSample).toBeLessThan(100); // No sustained 100% CPU
|
||||
expect(stressMessageCount / stressCpu.total).toBeGreaterThan(0.1); // At least 0.1 msg/ms efficiency
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
console.log(`\n${testId}: All ${scenarioCount} CPU utilization scenarios tested ✓`);
|
||||
});
|
@ -0,0 +1,686 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as plugins from './plugins.js';
|
||||
import { createTestServer } from '../../helpers/server.loader.js';
|
||||
import { createSmtpClient } from '../../helpers/smtp.client.js';
|
||||
|
||||
tap.test('CPERF-05: should optimize network efficiency', async (tools) => {
|
||||
const testId = 'CPERF-05-network-efficiency';
|
||||
console.log(`\n${testId}: Testing network efficiency optimization...`);
|
||||
|
||||
let scenarioCount = 0;
|
||||
|
||||
// Helper to track network activity
|
||||
class NetworkTracker {
|
||||
private startTime: number;
|
||||
private bytesSent: number = 0;
|
||||
private bytesReceived: number = 0;
|
||||
private connections: number = 0;
|
||||
private roundTrips: number = 0;
|
||||
|
||||
constructor() {
|
||||
this.startTime = Date.now();
|
||||
}
|
||||
|
||||
addConnection() {
|
||||
this.connections++;
|
||||
}
|
||||
|
||||
addBytesSent(bytes: number) {
|
||||
this.bytesSent += bytes;
|
||||
}
|
||||
|
||||
addBytesReceived(bytes: number) {
|
||||
this.bytesReceived += bytes;
|
||||
}
|
||||
|
||||
addRoundTrip() {
|
||||
this.roundTrips++;
|
||||
}
|
||||
|
||||
getStats() {
|
||||
const elapsed = Date.now() - this.startTime;
|
||||
return {
|
||||
elapsed,
|
||||
bytesSent: this.bytesSent,
|
||||
bytesReceived: this.bytesReceived,
|
||||
totalBytes: this.bytesSent + this.bytesReceived,
|
||||
connections: this.connections,
|
||||
roundTrips: this.roundTrips,
|
||||
bytesPerSecond: ((this.bytesSent + this.bytesReceived) / elapsed) * 1000,
|
||||
efficiency: this.bytesSent / (this.bytesSent + this.bytesReceived)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Scenario 1: Connection reuse efficiency
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing connection reuse efficiency`);
|
||||
|
||||
const tracker = new NetworkTracker();
|
||||
let connectionCount = 0;
|
||||
let totalCommandBytes = 0;
|
||||
let totalResponseBytes = 0;
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
connectionCount++;
|
||||
tracker.addConnection();
|
||||
console.log(` [Server] Connection ${connectionCount} established`);
|
||||
|
||||
const greeting = '220 reuse.example.com ESMTP\r\n';
|
||||
socket.write(greeting);
|
||||
tracker.addBytesSent(greeting.length);
|
||||
|
||||
socket.on('close', () => {
|
||||
console.log(` [Server] Connection ${connectionCount} closed`);
|
||||
});
|
||||
|
||||
socket.on('data', (data) => {
|
||||
totalCommandBytes += data.length;
|
||||
tracker.addBytesReceived(data.length);
|
||||
tracker.addRoundTrip();
|
||||
|
||||
const command = data.toString().trim();
|
||||
let response = '';
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
response = '250-reuse.example.com\r\n250 OK\r\n';
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
response = '250 OK\r\n';
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
response = '250 OK\r\n';
|
||||
} else if (command === 'DATA') {
|
||||
response = '354 Start mail input\r\n';
|
||||
} else if (command === '.') {
|
||||
response = '250 OK\r\n';
|
||||
} else if (command === 'RSET') {
|
||||
response = '250 OK\r\n';
|
||||
} else if (command === 'QUIT') {
|
||||
response = '221 Bye\r\n';
|
||||
}
|
||||
|
||||
if (response) {
|
||||
socket.write(response);
|
||||
totalResponseBytes += response.length;
|
||||
tracker.addBytesSent(response.length);
|
||||
}
|
||||
|
||||
if (command === 'QUIT') {
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Test individual connections vs reused connection
|
||||
const messageCount = 10;
|
||||
|
||||
// Individual connections approach
|
||||
console.log(` Testing ${messageCount} individual connections...`);
|
||||
const individualStart = Date.now();
|
||||
connectionCount = 0;
|
||||
totalCommandBytes = 0;
|
||||
totalResponseBytes = 0;
|
||||
|
||||
for (let i = 0; i < messageCount; i++) {
|
||||
const client = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`individual${i + 1}@example.com`],
|
||||
subject: `Individual connection test ${i + 1}`,
|
||||
text: `Testing individual connections - message ${i + 1}`
|
||||
});
|
||||
|
||||
await client.sendMail(email);
|
||||
|
||||
if (client.close) {
|
||||
await client.close();
|
||||
}
|
||||
}
|
||||
|
||||
const individualTime = Date.now() - individualStart;
|
||||
const individualStats = {
|
||||
connections: connectionCount,
|
||||
commandBytes: totalCommandBytes,
|
||||
responseBytes: totalResponseBytes,
|
||||
totalBytes: totalCommandBytes + totalResponseBytes,
|
||||
time: individualTime
|
||||
};
|
||||
|
||||
console.log(` Individual connections: ${individualStats.connections} connections, ${individualStats.totalBytes} bytes`);
|
||||
|
||||
// Connection reuse approach
|
||||
console.log(` Testing connection reuse...`);
|
||||
const reuseStart = Date.now();
|
||||
connectionCount = 0;
|
||||
totalCommandBytes = 0;
|
||||
totalResponseBytes = 0;
|
||||
|
||||
const reuseClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 1,
|
||||
maxMessages: messageCount
|
||||
});
|
||||
|
||||
const reuseEmails = Array(messageCount).fill(null).map((_, i) =>
|
||||
new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`reuse${i + 1}@example.com`],
|
||||
subject: `Connection reuse test ${i + 1}`,
|
||||
text: `Testing connection reuse - message ${i + 1}`
|
||||
})
|
||||
);
|
||||
|
||||
for (const email of reuseEmails) {
|
||||
await reuseClient.sendMail(email);
|
||||
}
|
||||
|
||||
await reuseClient.close();
|
||||
|
||||
const reuseTime = Date.now() - reuseStart;
|
||||
const reuseStats = {
|
||||
connections: connectionCount,
|
||||
commandBytes: totalCommandBytes,
|
||||
responseBytes: totalResponseBytes,
|
||||
totalBytes: totalCommandBytes + totalResponseBytes,
|
||||
time: reuseTime
|
||||
};
|
||||
|
||||
console.log(` Connection reuse: ${reuseStats.connections} connections, ${reuseStats.totalBytes} bytes`);
|
||||
|
||||
// Calculate efficiency
|
||||
const connectionEfficiency = individualStats.connections / reuseStats.connections;
|
||||
const byteEfficiency = individualStats.totalBytes / reuseStats.totalBytes;
|
||||
const timeEfficiency = individualTime / reuseTime;
|
||||
|
||||
console.log(` Connection efficiency: ${connectionEfficiency.toFixed(1)}x`);
|
||||
console.log(` Byte efficiency: ${byteEfficiency.toFixed(1)}x`);
|
||||
console.log(` Time efficiency: ${timeEfficiency.toFixed(1)}x`);
|
||||
|
||||
// Connection reuse should be more efficient
|
||||
expect(reuseStats.connections).toBeLessThan(individualStats.connections);
|
||||
expect(reuseStats.totalBytes).toBeLessThan(individualStats.totalBytes);
|
||||
expect(connectionEfficiency).toBeGreaterThan(5); // At least 5x fewer connections
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 2: Command pipelining efficiency
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing command pipelining efficiency`);
|
||||
|
||||
let totalCommands = 0;
|
||||
let pipelinedCommands = 0;
|
||||
let maxPipelineDepth = 0;
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected for pipelining test');
|
||||
socket.write('220 pipeline.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const commands = data.toString().split('\r\n').filter(cmd => cmd.length > 0);
|
||||
totalCommands += commands.length;
|
||||
|
||||
if (commands.length > 1) {
|
||||
pipelinedCommands += commands.length;
|
||||
maxPipelineDepth = Math.max(maxPipelineDepth, commands.length);
|
||||
console.log(` [Server] Received ${commands.length} pipelined commands`);
|
||||
}
|
||||
|
||||
commands.forEach(command => {
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-pipeline.example.com\r\n250-PIPELINING\r\n250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const pipelineClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pipelining: true
|
||||
});
|
||||
|
||||
// Send emails with multiple recipients (triggers pipelining)
|
||||
const emails = [
|
||||
new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient1@example.com', 'recipient2@example.com', 'recipient3@example.com'],
|
||||
subject: 'Pipelining test 1',
|
||||
text: 'Testing command pipelining efficiency'
|
||||
}),
|
||||
new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient4@example.com', 'recipient5@example.com'],
|
||||
subject: 'Pipelining test 2',
|
||||
text: 'Testing command pipelining efficiency'
|
||||
})
|
||||
];
|
||||
|
||||
console.log(' Sending emails with pipelining support...');
|
||||
for (const email of emails) {
|
||||
await pipelineClient.sendMail(email);
|
||||
}
|
||||
|
||||
console.log(` Total commands sent: ${totalCommands}`);
|
||||
console.log(` Pipelined commands: ${pipelinedCommands}`);
|
||||
console.log(` Max pipeline depth: ${maxPipelineDepth}`);
|
||||
console.log(` Pipelining efficiency: ${(pipelinedCommands / totalCommands * 100).toFixed(1)}%`);
|
||||
|
||||
// Should use pipelining for efficiency
|
||||
expect(pipelinedCommands).toBeGreaterThan(0);
|
||||
expect(maxPipelineDepth).toBeGreaterThan(1);
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 3: Message size optimization
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing message size optimization`);
|
||||
|
||||
let totalMessageBytes = 0;
|
||||
let messageCount = 0;
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected for size optimization test');
|
||||
socket.write('220 size.example.com ESMTP\r\n');
|
||||
|
||||
let inData = false;
|
||||
let messageBytes = 0;
|
||||
|
||||
socket.on('data', (data) => {
|
||||
if (inData) {
|
||||
messageBytes += data.length;
|
||||
if (data.toString().includes('\r\n.\r\n')) {
|
||||
inData = false;
|
||||
messageCount++;
|
||||
totalMessageBytes += messageBytes;
|
||||
console.log(` [Server] Message ${messageCount}: ${messageBytes} bytes`);
|
||||
socket.write('250 OK\r\n');
|
||||
messageBytes = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-size.example.com\r\n250-SIZE 52428800\r\n250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
inData = true;
|
||||
messageBytes = 0;
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const sizeClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Test different message sizes and encoding efficiency
|
||||
const sizeTests = [
|
||||
{
|
||||
name: 'Plain text',
|
||||
email: new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Plain text efficiency test',
|
||||
text: 'This is a plain text message for testing size efficiency.'
|
||||
})
|
||||
},
|
||||
{
|
||||
name: 'HTML message',
|
||||
email: new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'HTML efficiency test',
|
||||
html: '<html><body><h1>HTML Message</h1><p>This is an HTML message.</p></body></html>'
|
||||
})
|
||||
},
|
||||
{
|
||||
name: 'Multipart message',
|
||||
email: new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Multipart efficiency test',
|
||||
text: 'Plain text version of the message.',
|
||||
html: '<p>HTML version of the message.</p>'
|
||||
})
|
||||
},
|
||||
{
|
||||
name: 'Message with small attachment',
|
||||
email: new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Attachment efficiency test',
|
||||
text: 'Message with attachment.',
|
||||
attachments: [{
|
||||
filename: 'test.txt',
|
||||
content: 'Small test attachment content for efficiency testing.'
|
||||
}]
|
||||
})
|
||||
}
|
||||
];
|
||||
|
||||
for (const test of sizeTests) {
|
||||
console.log(` Testing ${test.name}...`);
|
||||
|
||||
const beforeBytes = totalMessageBytes;
|
||||
const beforeCount = messageCount;
|
||||
|
||||
await sizeClient.sendMail(test.email);
|
||||
|
||||
const messageSize = totalMessageBytes - beforeBytes;
|
||||
const overhead = messageSize / (test.email.text?.length || test.email.html?.length || 100);
|
||||
|
||||
console.log(` ${test.name}: ${messageSize} bytes (overhead: ${overhead.toFixed(1)}x)`);
|
||||
|
||||
// Message overhead should be reasonable
|
||||
expect(overhead).toBeLessThan(10); // Less than 10x overhead
|
||||
}
|
||||
|
||||
const avgMessageSize = totalMessageBytes / messageCount;
|
||||
console.log(` Average message size: ${avgMessageSize.toFixed(0)} bytes`);
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 4: Bandwidth utilization
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing bandwidth utilization`);
|
||||
|
||||
let totalBytes = 0;
|
||||
let dataTransferTime = 0;
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
socket.write('220 bandwidth.example.com ESMTP\r\n');
|
||||
|
||||
let transferStart = 0;
|
||||
let inData = false;
|
||||
|
||||
socket.on('data', (data) => {
|
||||
totalBytes += data.length;
|
||||
|
||||
if (inData) {
|
||||
if (data.toString().includes('\r\n.\r\n')) {
|
||||
inData = false;
|
||||
dataTransferTime += Date.now() - transferStart;
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-bandwidth.example.com\r\n250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
inData = true;
|
||||
transferStart = Date.now();
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const bandwidthClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Test bandwidth efficiency with varying message sizes
|
||||
const messageSizes = [1024, 10240, 102400]; // 1KB, 10KB, 100KB
|
||||
|
||||
console.log(' Testing bandwidth utilization with different message sizes...');
|
||||
const bandwidthStart = Date.now();
|
||||
|
||||
for (const size of messageSizes) {
|
||||
const content = 'x'.repeat(size);
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: `Bandwidth test ${size} bytes`,
|
||||
text: content
|
||||
});
|
||||
|
||||
await bandwidthClient.sendMail(email);
|
||||
}
|
||||
|
||||
const bandwidthElapsed = Date.now() - bandwidthStart;
|
||||
const throughput = (totalBytes / bandwidthElapsed) * 1000; // bytes per second
|
||||
const dataEfficiency = (messageSizes.reduce((a, b) => a + b, 0) / totalBytes) * 100;
|
||||
|
||||
console.log(` Total bytes transferred: ${totalBytes}`);
|
||||
console.log(` Data transfer time: ${dataTransferTime}ms`);
|
||||
console.log(` Overall throughput: ${(throughput / 1024).toFixed(1)} KB/s`);
|
||||
console.log(` Data efficiency: ${dataEfficiency.toFixed(1)}% (payload vs total)`);
|
||||
|
||||
// Bandwidth utilization should be efficient
|
||||
expect(throughput).toBeGreaterThan(1024); // At least 1KB/s
|
||||
expect(dataEfficiency).toBeGreaterThan(20); // At least 20% payload efficiency
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 5: Network round-trip optimization
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing network round-trip optimization`);
|
||||
|
||||
let roundTrips = 0;
|
||||
let commandCount = 0;
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
socket.write('220 roundtrip.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const commands = data.toString().split('\r\n').filter(cmd => cmd.length > 0);
|
||||
roundTrips++;
|
||||
commandCount += commands.length;
|
||||
|
||||
console.log(` [Server] Round-trip ${roundTrips}: ${commands.length} commands`);
|
||||
|
||||
commands.forEach(command => {
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-roundtrip.example.com\r\n250-PIPELINING\r\n250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const roundtripClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pipelining: true
|
||||
});
|
||||
|
||||
// Send email with multiple recipients to test round-trip efficiency
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['rcpt1@example.com', 'rcpt2@example.com', 'rcpt3@example.com', 'rcpt4@example.com'],
|
||||
subject: 'Round-trip optimization test',
|
||||
text: 'Testing network round-trip optimization with multiple recipients'
|
||||
});
|
||||
|
||||
console.log(' Sending email with multiple recipients...');
|
||||
await roundtripClient.sendMail(email);
|
||||
|
||||
const commandsPerRoundTrip = commandCount / roundTrips;
|
||||
const efficiency = commandsPerRoundTrip;
|
||||
|
||||
console.log(` Total round-trips: ${roundTrips}`);
|
||||
console.log(` Total commands: ${commandCount}`);
|
||||
console.log(` Commands per round-trip: ${commandsPerRoundTrip.toFixed(1)}`);
|
||||
console.log(` Round-trip efficiency: ${efficiency.toFixed(1)}`);
|
||||
|
||||
// Should minimize round-trips through pipelining
|
||||
expect(roundTrips).toBeLessThan(commandCount); // Fewer round-trips than commands
|
||||
expect(commandsPerRoundTrip).toBeGreaterThan(1.5); // At least 1.5 commands per round-trip
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 6: Connection pooling network efficiency
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing connection pooling network efficiency`);
|
||||
|
||||
let totalConnections = 0;
|
||||
let totalBytes = 0;
|
||||
let connectionSetupBytes = 0;
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
totalConnections++;
|
||||
const greeting = '220 pool.example.com ESMTP\r\n';
|
||||
socket.write(greeting);
|
||||
connectionSetupBytes += greeting.length;
|
||||
|
||||
console.log(` [Server] Pool connection ${totalConnections} established`);
|
||||
|
||||
socket.on('data', (data) => {
|
||||
totalBytes += data.length;
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
const response = '250-pool.example.com\r\n250 OK\r\n';
|
||||
socket.write(response);
|
||||
connectionSetupBytes += response.length;
|
||||
totalBytes += response.length;
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
const response = '250 OK\r\n';
|
||||
socket.write(response);
|
||||
totalBytes += response.length;
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
const response = '250 OK\r\n';
|
||||
socket.write(response);
|
||||
totalBytes += response.length;
|
||||
} else if (command === 'DATA') {
|
||||
const response = '354 Start mail input\r\n';
|
||||
socket.write(response);
|
||||
totalBytes += response.length;
|
||||
} else if (command === '.') {
|
||||
const response = '250 OK\r\n';
|
||||
socket.write(response);
|
||||
totalBytes += response.length;
|
||||
} else if (command === 'QUIT') {
|
||||
const response = '221 Bye\r\n';
|
||||
socket.write(response);
|
||||
totalBytes += response.length;
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const poolClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 3,
|
||||
maxMessages: 100
|
||||
});
|
||||
|
||||
// Send multiple emails through pool
|
||||
const emailCount = 15;
|
||||
const emails = Array(emailCount).fill(null).map((_, i) =>
|
||||
new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`pooled${i + 1}@example.com`],
|
||||
subject: `Pool efficiency test ${i + 1}`,
|
||||
text: `Testing pooled connection network efficiency - message ${i + 1}`
|
||||
})
|
||||
);
|
||||
|
||||
console.log(` Sending ${emailCount} emails through connection pool...`);
|
||||
const poolStart = Date.now();
|
||||
|
||||
await Promise.all(emails.map(email => poolClient.sendMail(email)));
|
||||
await poolClient.close();
|
||||
|
||||
const poolTime = Date.now() - poolStart;
|
||||
const setupOverhead = (connectionSetupBytes / totalBytes) * 100;
|
||||
const messagesPerConnection = emailCount / totalConnections;
|
||||
const bytesPerMessage = totalBytes / emailCount;
|
||||
|
||||
console.log(` Emails sent: ${emailCount}`);
|
||||
console.log(` Connections used: ${totalConnections}`);
|
||||
console.log(` Messages per connection: ${messagesPerConnection.toFixed(1)}`);
|
||||
console.log(` Total bytes: ${totalBytes}`);
|
||||
console.log(` Setup overhead: ${setupOverhead.toFixed(1)}%`);
|
||||
console.log(` Bytes per message: ${bytesPerMessage.toFixed(0)}`);
|
||||
console.log(` Network efficiency: ${(emailCount / totalConnections).toFixed(1)} msg/conn`);
|
||||
|
||||
// Connection pooling should be network efficient
|
||||
expect(totalConnections).toBeLessThan(emailCount); // Fewer connections than messages
|
||||
expect(messagesPerConnection).toBeGreaterThan(3); // At least 3 messages per connection
|
||||
expect(setupOverhead).toBeLessThan(20); // Less than 20% setup overhead
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
console.log(`\n${testId}: All ${scenarioCount} network efficiency scenarios tested ✓`);
|
||||
});
|
@ -0,0 +1,769 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as plugins from './plugins.js';
|
||||
import { createTestServer } from '../../helpers/server.loader.js';
|
||||
import { createSmtpClient } from '../../helpers/smtp.client.js';
|
||||
|
||||
tap.test('CPERF-06: should implement efficient caching strategies', async (tools) => {
|
||||
const testId = 'CPERF-06-caching-strategies';
|
||||
console.log(`\n${testId}: Testing caching strategies performance...`);
|
||||
|
||||
let scenarioCount = 0;
|
||||
|
||||
// Scenario 1: DNS resolution caching
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing DNS resolution caching`);
|
||||
|
||||
let dnsLookupCount = 0;
|
||||
const dnsCache = new Map<string, { address: string; timestamp: number }>();
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 dns-cache.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-dns-cache.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Simulate DNS lookup caching
|
||||
const mockDnsLookup = (hostname: string) => {
|
||||
const cached = dnsCache.get(hostname);
|
||||
const now = Date.now();
|
||||
|
||||
if (cached && (now - cached.timestamp) < 300000) { // 5 minute cache
|
||||
console.log(` [DNS] Cache hit for ${hostname}`);
|
||||
return cached.address;
|
||||
}
|
||||
|
||||
dnsLookupCount++;
|
||||
console.log(` [DNS] Cache miss for ${hostname} (lookup #${dnsLookupCount})`);
|
||||
|
||||
const address = testServer.hostname; // Mock resolution
|
||||
dnsCache.set(hostname, { address, timestamp: now });
|
||||
return address;
|
||||
};
|
||||
|
||||
// Test multiple connections to same host
|
||||
const connectionCount = 10;
|
||||
console.log(` Creating ${connectionCount} connections to test DNS caching...`);
|
||||
|
||||
const clients: any[] = [];
|
||||
const startTime = Date.now();
|
||||
|
||||
for (let i = 0; i < connectionCount; i++) {
|
||||
// Simulate DNS lookup
|
||||
const resolvedHost = mockDnsLookup(testServer.hostname);
|
||||
|
||||
const client = createSmtpClient({
|
||||
host: resolvedHost,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
clients.push(client);
|
||||
}
|
||||
|
||||
// Send emails through cached connections
|
||||
const emails = clients.map((client, i) =>
|
||||
new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i + 1}@example.com`],
|
||||
subject: `DNS cache test ${i + 1}`,
|
||||
text: `Testing DNS resolution caching - connection ${i + 1}`
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(emails.map((email, i) => clients[i].sendMail(email)));
|
||||
|
||||
const totalTime = Date.now() - startTime;
|
||||
const cacheHitRate = ((connectionCount - dnsLookupCount) / connectionCount) * 100;
|
||||
|
||||
console.log(` DNS lookups performed: ${dnsLookupCount}/${connectionCount}`);
|
||||
console.log(` Cache hit rate: ${cacheHitRate.toFixed(1)}%`);
|
||||
console.log(` Total time: ${totalTime}ms`);
|
||||
console.log(` Time per connection: ${(totalTime / connectionCount).toFixed(1)}ms`);
|
||||
|
||||
// Close clients
|
||||
await Promise.all(clients.map(client => {
|
||||
if (client.close) {
|
||||
return client.close();
|
||||
}
|
||||
return Promise.resolve();
|
||||
}));
|
||||
|
||||
// DNS caching should reduce lookups
|
||||
expect(dnsLookupCount).toBeLessThan(connectionCount);
|
||||
expect(cacheHitRate).toBeGreaterThan(50); // At least 50% cache hit rate
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 2: Connection pool caching
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing connection pool caching`);
|
||||
|
||||
let connectionCount = 0;
|
||||
let connectionReuse = 0;
|
||||
const connectionPool = new Map<string, { connection: any; lastUsed: number; messageCount: number }>();
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
connectionCount++;
|
||||
const connId = connectionCount;
|
||||
console.log(` [Server] New connection ${connId} created`);
|
||||
socket.write('220 pool-cache.example.com ESMTP\r\n');
|
||||
|
||||
let messageCount = 0;
|
||||
|
||||
socket.on('close', () => {
|
||||
console.log(` [Server] Connection ${connId} closed after ${messageCount} messages`);
|
||||
});
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-pool-cache.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
messageCount++;
|
||||
socket.write(`250 OK: Message ${messageCount} on connection ${connId}\r\n`);
|
||||
} else if (command === 'RSET') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Mock connection pool management
|
||||
const getPooledConnection = (key: string) => {
|
||||
const cached = connectionPool.get(key);
|
||||
const now = Date.now();
|
||||
|
||||
if (cached && (now - cached.lastUsed) < 60000) { // 1 minute idle timeout
|
||||
connectionReuse++;
|
||||
cached.lastUsed = now;
|
||||
cached.messageCount++;
|
||||
console.log(` [Pool] Reusing connection for ${key} (reuse #${connectionReuse})`);
|
||||
return cached.connection;
|
||||
}
|
||||
|
||||
console.log(` [Pool] Creating new connection for ${key}`);
|
||||
const newConnection = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 5,
|
||||
maxMessages: 10
|
||||
});
|
||||
|
||||
connectionPool.set(key, {
|
||||
connection: newConnection,
|
||||
lastUsed: now,
|
||||
messageCount: 0
|
||||
});
|
||||
|
||||
return newConnection;
|
||||
};
|
||||
|
||||
// Test connection reuse with same destination
|
||||
const destinations = [
|
||||
'example.com',
|
||||
'example.com', // Same as first (should reuse)
|
||||
'example.com', // Same as first (should reuse)
|
||||
'another.com',
|
||||
'example.com', // Back to first (should reuse)
|
||||
'another.com' // Same as fourth (should reuse)
|
||||
];
|
||||
|
||||
console.log(` Sending emails to test connection pool caching...`);
|
||||
|
||||
for (let i = 0; i < destinations.length; i++) {
|
||||
const destination = destinations[i];
|
||||
const poolKey = destination;
|
||||
|
||||
const client = getPooledConnection(poolKey);
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i + 1}@${destination}`],
|
||||
subject: `Pool cache test ${i + 1}`,
|
||||
text: `Testing connection pool caching - destination ${destination}`
|
||||
});
|
||||
|
||||
await client.sendMail(email);
|
||||
}
|
||||
|
||||
// Close all pooled connections
|
||||
for (const [key, pooled] of connectionPool) {
|
||||
if (pooled.connection.close) {
|
||||
await pooled.connection.close();
|
||||
}
|
||||
}
|
||||
|
||||
const uniqueDestinations = new Set(destinations).size;
|
||||
const poolEfficiency = (connectionReuse / destinations.length) * 100;
|
||||
|
||||
console.log(` Total emails sent: ${destinations.length}`);
|
||||
console.log(` Unique destinations: ${uniqueDestinations}`);
|
||||
console.log(` New connections: ${connectionCount}`);
|
||||
console.log(` Connection reuses: ${connectionReuse}`);
|
||||
console.log(` Pool efficiency: ${poolEfficiency.toFixed(1)}%`);
|
||||
|
||||
// Connection pool should reuse connections efficiently
|
||||
expect(connectionCount).toBeLessThanOrEqual(uniqueDestinations + 1);
|
||||
expect(connectionReuse).toBeGreaterThan(0);
|
||||
expect(poolEfficiency).toBeGreaterThan(30); // At least 30% reuse
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 3: Template and content caching
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing template and content caching`);
|
||||
|
||||
let templateCompilations = 0;
|
||||
let cacheHits = 0;
|
||||
const templateCache = new Map<string, { compiled: string; timestamp: number; uses: number }>();
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
socket.write('220 template-cache.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-template-cache.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Mock template compilation and caching
|
||||
const compileTemplate = (template: string, data: any) => {
|
||||
const cacheKey = template;
|
||||
const cached = templateCache.get(cacheKey);
|
||||
const now = Date.now();
|
||||
|
||||
if (cached && (now - cached.timestamp) < 3600000) { // 1 hour cache
|
||||
cacheHits++;
|
||||
cached.uses++;
|
||||
console.log(` [Template] Cache hit for template (use #${cached.uses})`);
|
||||
return cached.compiled.replace(/\{\{(\w+)\}\}/g, (match, key) => data[key] || match);
|
||||
}
|
||||
|
||||
templateCompilations++;
|
||||
console.log(` [Template] Compiling template (compilation #${templateCompilations})`);
|
||||
|
||||
// Simulate template compilation overhead
|
||||
const compiled = template.replace(/\{\{(\w+)\}\}/g, (match, key) => data[key] || match);
|
||||
|
||||
templateCache.set(cacheKey, {
|
||||
compiled: template, // Store template for reuse
|
||||
timestamp: now,
|
||||
uses: 1
|
||||
});
|
||||
|
||||
return compiled;
|
||||
};
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Test template caching with repeated templates
|
||||
const templates = [
|
||||
{
|
||||
id: 'welcome',
|
||||
subject: 'Welcome {{name}}!',
|
||||
text: 'Hello {{name}}, welcome to our service!'
|
||||
},
|
||||
{
|
||||
id: 'notification',
|
||||
subject: 'Notification for {{name}}',
|
||||
text: 'Dear {{name}}, you have a new notification.'
|
||||
},
|
||||
{
|
||||
id: 'welcome', // Repeat of first template
|
||||
subject: 'Welcome {{name}}!',
|
||||
text: 'Hello {{name}}, welcome to our service!'
|
||||
}
|
||||
];
|
||||
|
||||
const users = [
|
||||
{ name: 'Alice', email: 'alice@example.com' },
|
||||
{ name: 'Bob', email: 'bob@example.com' },
|
||||
{ name: 'Charlie', email: 'charlie@example.com' },
|
||||
{ name: 'Diana', email: 'diana@example.com' }
|
||||
];
|
||||
|
||||
console.log(' Sending templated emails to test content caching...');
|
||||
const startTime = Date.now();
|
||||
|
||||
for (const user of users) {
|
||||
for (const template of templates) {
|
||||
const compiledSubject = compileTemplate(template.subject, user);
|
||||
const compiledText = compileTemplate(template.text, user);
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: [user.email],
|
||||
subject: compiledSubject,
|
||||
text: compiledText
|
||||
});
|
||||
|
||||
await smtpClient.sendMail(email);
|
||||
}
|
||||
}
|
||||
|
||||
const totalTime = Date.now() - startTime;
|
||||
const totalTemplateUses = users.length * templates.length;
|
||||
const uniqueTemplates = new Set(templates.map(t => t.id)).size;
|
||||
const cacheEfficiency = (cacheHits / (templateCompilations + cacheHits)) * 100;
|
||||
|
||||
console.log(` Total template uses: ${totalTemplateUses}`);
|
||||
console.log(` Unique templates: ${uniqueTemplates}`);
|
||||
console.log(` Template compilations: ${templateCompilations}`);
|
||||
console.log(` Cache hits: ${cacheHits}`);
|
||||
console.log(` Cache efficiency: ${cacheEfficiency.toFixed(1)}%`);
|
||||
console.log(` Average time per email: ${(totalTime / totalTemplateUses).toFixed(1)}ms`);
|
||||
|
||||
// Template caching should reduce compilation overhead
|
||||
expect(templateCompilations).toBeLessThan(totalTemplateUses);
|
||||
expect(cacheHits).toBeGreaterThan(0);
|
||||
expect(cacheEfficiency).toBeGreaterThan(50); // At least 50% cache efficiency
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 4: Message header caching
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing message header caching`);
|
||||
|
||||
let headerGenerations = 0;
|
||||
let headerCacheHits = 0;
|
||||
const headerCache = new Map<string, { headers: any; timestamp: number }>();
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
socket.write('220 header-cache.example.com ESMTP\r\n');
|
||||
|
||||
let messageCount = 0;
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-header-cache.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
messageCount++;
|
||||
socket.write(`250 OK: Message ${messageCount} with cached headers\r\n`);
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Mock header generation and caching
|
||||
const generateHeaders = (from: string, subject: string, messageType: string) => {
|
||||
const cacheKey = `${from}-${messageType}`;
|
||||
const cached = headerCache.get(cacheKey);
|
||||
const now = Date.now();
|
||||
|
||||
if (cached && (now - cached.timestamp) < 1800000) { // 30 minute cache
|
||||
headerCacheHits++;
|
||||
console.log(` [Headers] Cache hit for ${messageType} headers`);
|
||||
return {
|
||||
...cached.headers,
|
||||
Subject: subject, // Subject is dynamic
|
||||
Date: new Date().toISOString(),
|
||||
'Message-ID': `<${Date.now()}-${Math.random()}@example.com>`
|
||||
};
|
||||
}
|
||||
|
||||
headerGenerations++;
|
||||
console.log(` [Headers] Generating ${messageType} headers (generation #${headerGenerations})`);
|
||||
|
||||
// Simulate header generation overhead
|
||||
const headers = {
|
||||
From: from,
|
||||
Subject: subject,
|
||||
Date: new Date().toISOString(),
|
||||
'Message-ID': `<${Date.now()}-${Math.random()}@example.com>`,
|
||||
'X-Mailer': 'Test Mailer 1.0',
|
||||
'MIME-Version': '1.0',
|
||||
'Content-Type': messageType === 'html' ? 'text/html; charset=UTF-8' : 'text/plain; charset=UTF-8'
|
||||
};
|
||||
|
||||
// Cache the static parts
|
||||
const cacheableHeaders = {
|
||||
From: from,
|
||||
'X-Mailer': 'Test Mailer 1.0',
|
||||
'MIME-Version': '1.0',
|
||||
'Content-Type': headers['Content-Type']
|
||||
};
|
||||
|
||||
headerCache.set(cacheKey, {
|
||||
headers: cacheableHeaders,
|
||||
timestamp: now
|
||||
});
|
||||
|
||||
return headers;
|
||||
};
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Test header caching with similar message types
|
||||
const messageTypes = ['text', 'html', 'text', 'html', 'text']; // Repeated types
|
||||
const sender = 'sender@example.com';
|
||||
|
||||
console.log(' Sending emails to test header caching...');
|
||||
|
||||
for (let i = 0; i < messageTypes.length; i++) {
|
||||
const messageType = messageTypes[i];
|
||||
const headers = generateHeaders(sender, `Test ${i + 1}`, messageType);
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: headers.From,
|
||||
to: [`recipient${i + 1}@example.com`],
|
||||
subject: headers.Subject,
|
||||
text: messageType === 'text' ? 'Plain text message' : undefined,
|
||||
html: messageType === 'html' ? '<p>HTML message</p>' : undefined,
|
||||
headers: {
|
||||
'X-Mailer': headers['X-Mailer'],
|
||||
'Message-ID': headers['Message-ID']
|
||||
}
|
||||
});
|
||||
|
||||
await smtpClient.sendMail(email);
|
||||
}
|
||||
|
||||
const uniqueMessageTypes = new Set(messageTypes).size;
|
||||
const headerCacheEfficiency = (headerCacheHits / (headerGenerations + headerCacheHits)) * 100;
|
||||
|
||||
console.log(` Messages sent: ${messageTypes.length}`);
|
||||
console.log(` Unique message types: ${uniqueMessageTypes}`);
|
||||
console.log(` Header generations: ${headerGenerations}`);
|
||||
console.log(` Header cache hits: ${headerCacheHits}`);
|
||||
console.log(` Header cache efficiency: ${headerCacheEfficiency.toFixed(1)}%`);
|
||||
|
||||
// Header caching should reduce generation overhead
|
||||
expect(headerGenerations).toBeLessThan(messageTypes.length);
|
||||
expect(headerCacheHits).toBeGreaterThan(0);
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 5: Attachment processing caching
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing attachment processing caching`);
|
||||
|
||||
let attachmentProcessing = 0;
|
||||
let attachmentCacheHits = 0;
|
||||
const attachmentCache = new Map<string, { processed: string; timestamp: number; size: number }>();
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
socket.write('220 attachment-cache.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-attachment-cache.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Mock attachment processing with caching
|
||||
const processAttachment = (filename: string, content: Buffer) => {
|
||||
const contentHash = require('crypto').createHash('md5').update(content).digest('hex');
|
||||
const cacheKey = `${filename}-${contentHash}`;
|
||||
const cached = attachmentCache.get(cacheKey);
|
||||
const now = Date.now();
|
||||
|
||||
if (cached && (now - cached.timestamp) < 7200000) { // 2 hour cache
|
||||
attachmentCacheHits++;
|
||||
console.log(` [Attachment] Cache hit for ${filename}`);
|
||||
return cached.processed;
|
||||
}
|
||||
|
||||
attachmentProcessing++;
|
||||
console.log(` [Attachment] Processing ${filename} (processing #${attachmentProcessing})`);
|
||||
|
||||
// Simulate attachment processing (base64 encoding)
|
||||
const processed = content.toString('base64');
|
||||
|
||||
attachmentCache.set(cacheKey, {
|
||||
processed,
|
||||
timestamp: now,
|
||||
size: content.length
|
||||
});
|
||||
|
||||
return processed;
|
||||
};
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Create reusable attachment content
|
||||
const commonAttachment = Buffer.from('This is a common attachment used in multiple emails.');
|
||||
const uniqueAttachment = Buffer.from('This is a unique attachment.');
|
||||
|
||||
const emails = [
|
||||
{
|
||||
subject: 'Email 1 with common attachment',
|
||||
attachments: [{ filename: 'common.txt', content: commonAttachment }]
|
||||
},
|
||||
{
|
||||
subject: 'Email 2 with unique attachment',
|
||||
attachments: [{ filename: 'unique.txt', content: uniqueAttachment }]
|
||||
},
|
||||
{
|
||||
subject: 'Email 3 with common attachment again',
|
||||
attachments: [{ filename: 'common.txt', content: commonAttachment }] // Same as first
|
||||
},
|
||||
{
|
||||
subject: 'Email 4 with both attachments',
|
||||
attachments: [
|
||||
{ filename: 'common.txt', content: commonAttachment },
|
||||
{ filename: 'unique.txt', content: uniqueAttachment }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
console.log(' Sending emails with attachments to test caching...');
|
||||
|
||||
for (let i = 0; i < emails.length; i++) {
|
||||
const emailData = emails[i];
|
||||
|
||||
// Process attachments (with caching)
|
||||
const processedAttachments = emailData.attachments.map(att => ({
|
||||
filename: att.filename,
|
||||
content: processAttachment(att.filename, att.content)
|
||||
}));
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i + 1}@example.com`],
|
||||
subject: emailData.subject,
|
||||
text: 'Email with attachments for caching test',
|
||||
attachments: processedAttachments.map(att => ({
|
||||
filename: att.filename,
|
||||
content: att.content,
|
||||
encoding: 'base64' as const
|
||||
}))
|
||||
});
|
||||
|
||||
await smtpClient.sendMail(email);
|
||||
}
|
||||
|
||||
const totalAttachments = emails.reduce((sum, email) => sum + email.attachments.length, 0);
|
||||
const attachmentCacheEfficiency = (attachmentCacheHits / (attachmentProcessing + attachmentCacheHits)) * 100;
|
||||
|
||||
console.log(` Total attachments sent: ${totalAttachments}`);
|
||||
console.log(` Attachment processing operations: ${attachmentProcessing}`);
|
||||
console.log(` Attachment cache hits: ${attachmentCacheHits}`);
|
||||
console.log(` Attachment cache efficiency: ${attachmentCacheEfficiency.toFixed(1)}%`);
|
||||
|
||||
// Attachment caching should reduce processing overhead
|
||||
expect(attachmentProcessing).toBeLessThan(totalAttachments);
|
||||
expect(attachmentCacheHits).toBeGreaterThan(0);
|
||||
expect(attachmentCacheEfficiency).toBeGreaterThan(30); // At least 30% cache efficiency
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 6: Overall caching performance impact
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing overall caching performance impact`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
socket.write('220 performance-cache.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-performance-cache.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Test performance with caching enabled vs disabled
|
||||
const emailCount = 20;
|
||||
|
||||
// Simulate no caching (always process)
|
||||
console.log(' Testing performance without caching...');
|
||||
const noCacheStart = Date.now();
|
||||
let noCacheOperations = 0;
|
||||
|
||||
const noCacheClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
for (let i = 0; i < emailCount; i++) {
|
||||
// Simulate processing overhead for each email
|
||||
noCacheOperations += 3; // DNS lookup, header generation, template processing
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`nocache${i + 1}@example.com`],
|
||||
subject: `No cache test ${i + 1}`,
|
||||
text: `Testing performance without caching - email ${i + 1}`
|
||||
});
|
||||
|
||||
await noCacheClient.sendMail(email);
|
||||
}
|
||||
|
||||
const noCacheTime = Date.now() - noCacheStart;
|
||||
|
||||
// Simulate with caching (reduced processing)
|
||||
console.log(' Testing performance with caching...');
|
||||
const cacheStart = Date.now();
|
||||
let cacheOperations = 5; // Initial setup, then reuse
|
||||
|
||||
const cacheClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 2
|
||||
});
|
||||
|
||||
for (let i = 0; i < emailCount; i++) {
|
||||
// Simulate reduced operations due to caching
|
||||
if (i < 5) {
|
||||
cacheOperations += 1; // Some cache misses initially
|
||||
}
|
||||
// Most operations are cache hits (no additional operations)
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`cache${i + 1}@example.com`],
|
||||
subject: `Cache test ${i + 1}`,
|
||||
text: `Testing performance with caching - email ${i + 1}`
|
||||
});
|
||||
|
||||
await cacheClient.sendMail(email);
|
||||
}
|
||||
|
||||
await cacheClient.close();
|
||||
const cacheTime = Date.now() - cacheStart;
|
||||
|
||||
// Calculate performance improvements
|
||||
const timeImprovement = ((noCacheTime - cacheTime) / noCacheTime) * 100;
|
||||
const operationReduction = ((noCacheOperations - cacheOperations) / noCacheOperations) * 100;
|
||||
const throughputImprovement = (emailCount / cacheTime) / (emailCount / noCacheTime);
|
||||
|
||||
console.log(` Performance comparison (${emailCount} emails):`);
|
||||
console.log(` Without caching: ${noCacheTime}ms, ${noCacheOperations} operations`);
|
||||
console.log(` With caching: ${cacheTime}ms, ${cacheOperations} operations`);
|
||||
console.log(` Time improvement: ${timeImprovement.toFixed(1)}%`);
|
||||
console.log(` Operation reduction: ${operationReduction.toFixed(1)}%`);
|
||||
console.log(` Throughput improvement: ${throughputImprovement.toFixed(2)}x`);
|
||||
|
||||
// Caching should improve performance
|
||||
expect(cacheTime).toBeLessThan(noCacheTime);
|
||||
expect(cacheOperations).toBeLessThan(noCacheOperations);
|
||||
expect(timeImprovement).toBeGreaterThan(10); // At least 10% improvement
|
||||
expect(throughputImprovement).toBeGreaterThan(1.1); // At least 10% better throughput
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
console.log(`\n${testId}: All ${scenarioCount} caching strategy scenarios tested ✓`);
|
||||
});
|
@ -0,0 +1,408 @@
|
||||
import { test } from '@git.zone/tstest/tapbundle';
|
||||
import { createTestServer, createSmtpClient } from '../../helpers/utils.js';
|
||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
||||
|
||||
test('CPERF-07: Queue Management Performance Tests', async () => {
|
||||
console.log('\n🚀 Testing SMTP Client Queue Management Performance');
|
||||
console.log('=' .repeat(60));
|
||||
|
||||
// Scenario 1: Queue Processing Speed
|
||||
await test.test('Scenario 1: Queue Processing Speed', async () => {
|
||||
console.log('\n📊 Testing queue processing speed and throughput...');
|
||||
|
||||
const testServer = await createTestServer({
|
||||
responseDelay: 50, // 50ms delay per message
|
||||
onConnect: (socket: any) => {
|
||||
console.log(' [Server] Client connected for queue speed test');
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 3,
|
||||
maxMessages: 50,
|
||||
rateDelta: 1000,
|
||||
rateLimit: 10 // 10 emails per second
|
||||
});
|
||||
|
||||
try {
|
||||
console.log(' Creating 25 test emails for queue processing...');
|
||||
const emails = [];
|
||||
for (let i = 0; i < 25; i++) {
|
||||
emails.push(new Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i}@example.com`],
|
||||
subject: `Queue Test Email ${i + 1}`,
|
||||
text: `This is queue test email number ${i + 1}`,
|
||||
messageId: `queue-test-${i + 1}@example.com`
|
||||
}));
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
console.log(' Starting bulk queue processing...');
|
||||
|
||||
const promises = emails.map((email, index) => {
|
||||
return smtpClient.sendMail(email).then(result => {
|
||||
console.log(` ✓ Email ${index + 1} processed: ${result.messageId}`);
|
||||
return { index, result, timestamp: Date.now() };
|
||||
}).catch(error => {
|
||||
console.log(` ✗ Email ${index + 1} failed: ${error.message}`);
|
||||
return { index, error, timestamp: Date.now() };
|
||||
});
|
||||
});
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
const endTime = Date.now();
|
||||
const totalTime = endTime - startTime;
|
||||
const throughput = (emails.length / totalTime) * 1000; // emails per second
|
||||
|
||||
console.log(` Queue processing completed in ${totalTime}ms`);
|
||||
console.log(` Throughput: ${throughput.toFixed(2)} emails/second`);
|
||||
console.log(` Success rate: ${results.filter(r => !r.error).length}/${emails.length}`);
|
||||
|
||||
} finally {
|
||||
smtpClient.close();
|
||||
testServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Scenario 2: Queue Priority Management
|
||||
await test.test('Scenario 2: Queue Priority Management', async () => {
|
||||
console.log('\n🎯 Testing queue priority and email ordering...');
|
||||
|
||||
const processedOrder: string[] = [];
|
||||
const testServer = await createTestServer({
|
||||
responseDelay: 10,
|
||||
onData: (data: string, socket: any) => {
|
||||
if (data.includes('Subject: HIGH PRIORITY')) {
|
||||
processedOrder.push('HIGH');
|
||||
} else if (data.includes('Subject: NORMAL PRIORITY')) {
|
||||
processedOrder.push('NORMAL');
|
||||
} else if (data.includes('Subject: LOW PRIORITY')) {
|
||||
processedOrder.push('LOW');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 1 // Single connection to test ordering
|
||||
});
|
||||
|
||||
try {
|
||||
console.log(' Creating emails with different priorities...');
|
||||
|
||||
// Create emails in mixed order but with priority headers
|
||||
const emails = [
|
||||
new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient1@example.com'],
|
||||
subject: 'LOW PRIORITY Email 1',
|
||||
text: 'Low priority content',
|
||||
priority: 'low',
|
||||
headers: { 'X-Priority': '5' }
|
||||
}),
|
||||
new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient2@example.com'],
|
||||
subject: 'HIGH PRIORITY Email 1',
|
||||
text: 'High priority content',
|
||||
priority: 'high',
|
||||
headers: { 'X-Priority': '1' }
|
||||
}),
|
||||
new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient3@example.com'],
|
||||
subject: 'NORMAL PRIORITY Email 1',
|
||||
text: 'Normal priority content',
|
||||
priority: 'normal',
|
||||
headers: { 'X-Priority': '3' }
|
||||
}),
|
||||
new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient4@example.com'],
|
||||
subject: 'HIGH PRIORITY Email 2',
|
||||
text: 'Another high priority',
|
||||
priority: 'high',
|
||||
headers: { 'X-Priority': '1' }
|
||||
})
|
||||
];
|
||||
|
||||
console.log(' Sending emails and monitoring processing order...');
|
||||
|
||||
// Send all emails simultaneously
|
||||
const promises = emails.map((email, index) => {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
smtpClient.sendMail(email).then(resolve).catch(resolve);
|
||||
}, index * 20); // Small delays to ensure ordering
|
||||
});
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
// Wait for all processing to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
console.log(` Processing order: ${processedOrder.join(' -> ')}`);
|
||||
console.log(` Expected high priority emails to be processed first`);
|
||||
|
||||
// Count priority distribution
|
||||
const highCount = processedOrder.filter(p => p === 'HIGH').length;
|
||||
const normalCount = processedOrder.filter(p => p === 'NORMAL').length;
|
||||
const lowCount = processedOrder.filter(p => p === 'LOW').length;
|
||||
|
||||
console.log(` High: ${highCount}, Normal: ${normalCount}, Low: ${lowCount}`);
|
||||
|
||||
} finally {
|
||||
smtpClient.close();
|
||||
testServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Scenario 3: Queue Size Management
|
||||
await test.test('Scenario 3: Queue Size Management', async () => {
|
||||
console.log('\n📈 Testing queue size limits and overflow handling...');
|
||||
|
||||
let connectionCount = 0;
|
||||
const testServer = await createTestServer({
|
||||
responseDelay: 100, // Slow responses to build up queue
|
||||
onConnect: () => {
|
||||
connectionCount++;
|
||||
console.log(` [Server] Connection ${connectionCount} established`);
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 2,
|
||||
maxMessages: 5, // Low limit to test overflow
|
||||
queueSize: 10
|
||||
});
|
||||
|
||||
try {
|
||||
console.log(' Creating 15 emails to test queue overflow...');
|
||||
|
||||
const emails = [];
|
||||
for (let i = 0; i < 15; i++) {
|
||||
emails.push(new Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i}@example.com`],
|
||||
subject: `Queue Size Test ${i + 1}`,
|
||||
text: `Testing queue management ${i + 1}`,
|
||||
messageId: `queue-size-${i + 1}@example.com`
|
||||
}));
|
||||
}
|
||||
|
||||
console.log(' Sending emails rapidly to fill queue...');
|
||||
const startTime = Date.now();
|
||||
const results = [];
|
||||
|
||||
// Send emails in rapid succession
|
||||
for (let i = 0; i < emails.length; i++) {
|
||||
try {
|
||||
const promise = smtpClient.sendMail(emails[i]);
|
||||
results.push(promise);
|
||||
console.log(` 📤 Email ${i + 1} queued`);
|
||||
|
||||
// Small delay between sends
|
||||
if (i < emails.length - 1) {
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(` ❌ Email ${i + 1} rejected: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(' Waiting for queue processing to complete...');
|
||||
const finalResults = await Promise.allSettled(results);
|
||||
const endTime = Date.now();
|
||||
|
||||
const successful = finalResults.filter(r => r.status === 'fulfilled').length;
|
||||
const failed = finalResults.filter(r => r.status === 'rejected').length;
|
||||
|
||||
console.log(` Queue processing completed in ${endTime - startTime}ms`);
|
||||
console.log(` Successful: ${successful}, Failed: ${failed}`);
|
||||
console.log(` Max connections used: ${connectionCount}`);
|
||||
console.log(` Queue overflow handling: ${failed > 0 ? 'Detected' : 'None'}`);
|
||||
|
||||
} finally {
|
||||
smtpClient.close();
|
||||
testServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Scenario 4: Queue Recovery After Failures
|
||||
await test.test('Scenario 4: Queue Recovery After Failures', async () => {
|
||||
console.log('\n🔄 Testing queue recovery after connection failures...');
|
||||
|
||||
let connectionAttempts = 0;
|
||||
let shouldFail = true;
|
||||
|
||||
const testServer = await createTestServer({
|
||||
responseDelay: 50,
|
||||
onConnect: (socket: any) => {
|
||||
connectionAttempts++;
|
||||
console.log(` [Server] Connection attempt ${connectionAttempts}`);
|
||||
|
||||
if (shouldFail && connectionAttempts <= 3) {
|
||||
console.log(` [Server] Simulating connection failure ${connectionAttempts}`);
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
// After 3 failures, allow connections
|
||||
shouldFail = false;
|
||||
console.log(` [Server] Connection successful on attempt ${connectionAttempts}`);
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 2,
|
||||
maxMessages: 100,
|
||||
// Retry configuration
|
||||
retryDelay: 100,
|
||||
retries: 5
|
||||
});
|
||||
|
||||
try {
|
||||
console.log(' Creating emails that will initially fail...');
|
||||
|
||||
const emails = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
emails.push(new Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i}@example.com`],
|
||||
subject: `Recovery Test ${i + 1}`,
|
||||
text: `Testing queue recovery ${i + 1}`,
|
||||
messageId: `recovery-${i + 1}@example.com`
|
||||
}));
|
||||
}
|
||||
|
||||
console.log(' Sending emails (expecting initial failures)...');
|
||||
const startTime = Date.now();
|
||||
|
||||
const promises = emails.map((email, index) => {
|
||||
return smtpClient.sendMail(email).then(result => {
|
||||
console.log(` ✓ Email ${index + 1} sent successfully after recovery`);
|
||||
return { success: true, result };
|
||||
}).catch(error => {
|
||||
console.log(` ✗ Email ${index + 1} permanently failed: ${error.message}`);
|
||||
return { success: false, error };
|
||||
});
|
||||
});
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
const endTime = Date.now();
|
||||
|
||||
const successful = results.filter(r => r.success).length;
|
||||
const failed = results.filter(r => !r.success).length;
|
||||
|
||||
console.log(` Recovery test completed in ${endTime - startTime}ms`);
|
||||
console.log(` Connection attempts: ${connectionAttempts}`);
|
||||
console.log(` Successful after recovery: ${successful}`);
|
||||
console.log(` Permanently failed: ${failed}`);
|
||||
console.log(` Recovery rate: ${((successful / emails.length) * 100).toFixed(1)}%`);
|
||||
|
||||
} finally {
|
||||
smtpClient.close();
|
||||
testServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Scenario 5: Concurrent Queue Operations
|
||||
await test.test('Scenario 5: Concurrent Queue Operations', async () => {
|
||||
console.log('\n⚡ Testing concurrent queue operations and thread safety...');
|
||||
|
||||
let messageCount = 0;
|
||||
const testServer = await createTestServer({
|
||||
responseDelay: 20,
|
||||
onData: (data: string) => {
|
||||
if (data.includes('DATA')) {
|
||||
messageCount++;
|
||||
console.log(` [Server] Processing message ${messageCount}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 4,
|
||||
maxMessages: 25
|
||||
});
|
||||
|
||||
try {
|
||||
console.log(' Starting multiple concurrent queue operations...');
|
||||
|
||||
// Create multiple batches of emails
|
||||
const batches = [];
|
||||
for (let batch = 0; batch < 3; batch++) {
|
||||
const batchEmails = [];
|
||||
for (let i = 0; i < 8; i++) {
|
||||
batchEmails.push(new Email({
|
||||
from: `sender${batch}@example.com`,
|
||||
to: [`recipient${batch}-${i}@example.com`],
|
||||
subject: `Concurrent Batch ${batch + 1} Email ${i + 1}`,
|
||||
text: `Concurrent processing test batch ${batch + 1}, email ${i + 1}`,
|
||||
messageId: `concurrent-${batch}-${i}@example.com`
|
||||
}));
|
||||
}
|
||||
batches.push(batchEmails);
|
||||
}
|
||||
|
||||
console.log(' Launching concurrent batch operations...');
|
||||
const startTime = Date.now();
|
||||
|
||||
const batchPromises = batches.map((batchEmails, batchIndex) => {
|
||||
return Promise.all(batchEmails.map((email, emailIndex) => {
|
||||
return smtpClient.sendMail(email).then(result => {
|
||||
console.log(` ✓ Batch ${batchIndex + 1}, Email ${emailIndex + 1} sent`);
|
||||
return { batch: batchIndex, email: emailIndex, success: true };
|
||||
}).catch(error => {
|
||||
console.log(` ✗ Batch ${batchIndex + 1}, Email ${emailIndex + 1} failed`);
|
||||
return { batch: batchIndex, email: emailIndex, success: false, error };
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
const batchResults = await Promise.all(batchPromises);
|
||||
const endTime = Date.now();
|
||||
|
||||
// Flatten results
|
||||
const allResults = batchResults.flat();
|
||||
const totalEmails = allResults.length;
|
||||
const successful = allResults.filter(r => r.success).length;
|
||||
const failed = totalEmails - successful;
|
||||
|
||||
console.log(` Concurrent operations completed in ${endTime - startTime}ms`);
|
||||
console.log(` Total emails processed: ${totalEmails}`);
|
||||
console.log(` Successful: ${successful}, Failed: ${failed}`);
|
||||
console.log(` Success rate: ${((successful / totalEmails) * 100).toFixed(1)}%`);
|
||||
console.log(` Server processed: ${messageCount} messages`);
|
||||
console.log(` Concurrency efficiency: ${messageCount === successful ? 'Perfect' : 'Partial'}`);
|
||||
|
||||
} finally {
|
||||
smtpClient.close();
|
||||
testServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
console.log('\n✅ CPERF-07: Queue Management Performance Tests completed');
|
||||
console.log('📊 All queue management scenarios tested successfully');
|
||||
});
|
533
test/suite/smtpclient_performance/test.cperf-08.dns-caching.ts
Normal file
533
test/suite/smtpclient_performance/test.cperf-08.dns-caching.ts
Normal file
@ -0,0 +1,533 @@
|
||||
import { test } from '@git.zone/tstest/tapbundle';
|
||||
import { createTestServer, createSmtpClient } from '../../helpers/utils.js';
|
||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
||||
|
||||
test('CPERF-08: DNS Caching Efficiency Performance Tests', async () => {
|
||||
console.log('\n🌐 Testing SMTP Client DNS Caching Efficiency');
|
||||
console.log('=' .repeat(60));
|
||||
|
||||
// Scenario 1: DNS Resolution Caching
|
||||
await test.test('Scenario 1: DNS Resolution Caching', async () => {
|
||||
console.log('\n🔍 Testing DNS resolution caching performance...');
|
||||
|
||||
let dnsLookupCount = 0;
|
||||
const originalLookup = require('dns').lookup;
|
||||
|
||||
// Mock DNS lookup to track calls
|
||||
require('dns').lookup = (hostname: string, options: any, callback: any) => {
|
||||
dnsLookupCount++;
|
||||
console.log(` [DNS] Lookup ${dnsLookupCount} for: ${hostname}`);
|
||||
|
||||
// Simulate DNS resolution delay
|
||||
setTimeout(() => {
|
||||
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
||||
callback(null, '127.0.0.1', 4);
|
||||
} else {
|
||||
callback(null, '127.0.0.1', 4); // Mock all domains to localhost for testing
|
||||
}
|
||||
}, 50); // 50ms DNS lookup delay
|
||||
};
|
||||
|
||||
const testServer = await createTestServer({
|
||||
responseDelay: 10,
|
||||
onConnect: () => {
|
||||
console.log(' [Server] Connection established');
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
console.log(' Creating SMTP client with connection pooling...');
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 3,
|
||||
maxMessages: 100,
|
||||
// DNS caching settings
|
||||
dnsCache: true,
|
||||
dnsCacheTtl: 5000 // 5 seconds TTL
|
||||
});
|
||||
|
||||
console.log(' Sending multiple emails to same domain...');
|
||||
const emails = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
emails.push(new Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i}@example.com`],
|
||||
subject: `DNS Cache Test ${i + 1}`,
|
||||
text: `Testing DNS caching efficiency ${i + 1}`,
|
||||
messageId: `dns-cache-${i + 1}@example.com`
|
||||
}));
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
const promises = emails.map((email, index) => {
|
||||
return smtpClient.sendMail(email).then(result => {
|
||||
console.log(` ✓ Email ${index + 1} sent successfully`);
|
||||
return { success: true, result };
|
||||
}).catch(error => {
|
||||
console.log(` ✗ Email ${index + 1} failed: ${error.message}`);
|
||||
return { success: false, error };
|
||||
});
|
||||
});
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
const endTime = Date.now();
|
||||
|
||||
const successful = results.filter(r => r.success).length;
|
||||
const totalTime = endTime - startTime;
|
||||
|
||||
console.log(` Total DNS lookups performed: ${dnsLookupCount}`);
|
||||
console.log(` Emails sent: ${emails.length}, Successful: ${successful}`);
|
||||
console.log(` Total time: ${totalTime}ms`);
|
||||
console.log(` DNS cache efficiency: ${dnsLookupCount < emails.length ? 'Good' : 'Poor'}`);
|
||||
console.log(` Expected 1-3 DNS lookups for ${emails.length} emails to same domain`);
|
||||
|
||||
smtpClient.close();
|
||||
} finally {
|
||||
// Restore original DNS lookup
|
||||
require('dns').lookup = originalLookup;
|
||||
testServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Scenario 2: Multiple Domain DNS Caching
|
||||
await test.test('Scenario 2: Multiple Domain DNS Caching', async () => {
|
||||
console.log('\n🌍 Testing DNS caching across multiple domains...');
|
||||
|
||||
let dnsLookupCount = 0;
|
||||
const dnsCache = new Map<string, { ip: string; timestamp: number }>();
|
||||
const originalLookup = require('dns').lookup;
|
||||
|
||||
// Enhanced DNS mock with caching simulation
|
||||
require('dns').lookup = (hostname: string, options: any, callback: any) => {
|
||||
const now = Date.now();
|
||||
const cached = dnsCache.get(hostname);
|
||||
|
||||
if (cached && (now - cached.timestamp) < 3000) { // 3 second cache
|
||||
console.log(` [DNS] Cache hit for: ${hostname}`);
|
||||
setTimeout(() => callback(null, cached.ip, 4), 5); // Fast cache response
|
||||
return;
|
||||
}
|
||||
|
||||
dnsLookupCount++;
|
||||
console.log(` [DNS] Cache miss, lookup ${dnsLookupCount} for: ${hostname}`);
|
||||
|
||||
setTimeout(() => {
|
||||
const ip = '127.0.0.1';
|
||||
dnsCache.set(hostname, { ip, timestamp: now });
|
||||
callback(null, ip, 4);
|
||||
}, 75); // Slower DNS lookup
|
||||
};
|
||||
|
||||
const testServer = await createTestServer({
|
||||
responseDelay: 10
|
||||
});
|
||||
|
||||
try {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 2,
|
||||
dnsCache: true,
|
||||
dnsCacheTtl: 3000
|
||||
});
|
||||
|
||||
console.log(' Creating emails to multiple domains...');
|
||||
const domains = ['domain1.com', 'domain2.com', 'domain3.com'];
|
||||
const emails = [];
|
||||
|
||||
// Create multiple emails per domain
|
||||
domains.forEach((domain, domainIndex) => {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
emails.push(new Email({
|
||||
from: `sender@${domain}`,
|
||||
to: [`recipient${i}@${domain}`],
|
||||
subject: `Multi-domain DNS Test ${domainIndex + 1}-${i + 1}`,
|
||||
text: `Testing DNS caching for ${domain}`,
|
||||
messageId: `multi-dns-${domainIndex}-${i}@${domain}`
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
console.log(' Sending emails to test DNS caching across domains...');
|
||||
const startTime = Date.now();
|
||||
|
||||
const results = [];
|
||||
for (const email of emails) {
|
||||
try {
|
||||
const result = await smtpClient.sendMail(email);
|
||||
results.push({ success: true, result });
|
||||
console.log(` ✓ Email to ${email.to[0]} sent`);
|
||||
} catch (error) {
|
||||
results.push({ success: false, error });
|
||||
console.log(` ✗ Email to ${email.to[0]} failed`);
|
||||
}
|
||||
|
||||
// Small delay between sends
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
}
|
||||
|
||||
const endTime = Date.now();
|
||||
const successful = results.filter(r => r.success).length;
|
||||
|
||||
console.log(` Total DNS lookups: ${dnsLookupCount}`);
|
||||
console.log(` Unique domains: ${domains.length}`);
|
||||
console.log(` Total emails: ${emails.length}, Successful: ${successful}`);
|
||||
console.log(` Total time: ${endTime - startTime}ms`);
|
||||
console.log(` DNS cache entries: ${dnsCache.size}`);
|
||||
console.log(` Expected ~${domains.length} DNS lookups for ${domains.length} domains`);
|
||||
|
||||
smtpClient.close();
|
||||
} finally {
|
||||
require('dns').lookup = originalLookup;
|
||||
testServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Scenario 3: DNS Cache TTL and Refresh
|
||||
await test.test('Scenario 3: DNS Cache TTL and Refresh', async () => {
|
||||
console.log('\n⏰ Testing DNS cache TTL and refresh behavior...');
|
||||
|
||||
let dnsLookupCount = 0;
|
||||
const lookupTimes: number[] = [];
|
||||
const originalLookup = require('dns').lookup;
|
||||
|
||||
require('dns').lookup = (hostname: string, options: any, callback: any) => {
|
||||
dnsLookupCount++;
|
||||
const lookupTime = Date.now();
|
||||
lookupTimes.push(lookupTime);
|
||||
console.log(` [DNS] Lookup ${dnsLookupCount} at ${new Date(lookupTime).toISOString().substr(11, 12)}`);
|
||||
|
||||
setTimeout(() => {
|
||||
callback(null, '127.0.0.1', 4);
|
||||
}, 40);
|
||||
};
|
||||
|
||||
const testServer = await createTestServer({
|
||||
responseDelay: 10
|
||||
});
|
||||
|
||||
try {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 1,
|
||||
dnsCache: true,
|
||||
dnsCacheTtl: 1000 // 1 second TTL for testing
|
||||
});
|
||||
|
||||
console.log(' Sending emails with 1.5 second intervals to test TTL...');
|
||||
|
||||
const email1 = new Email({
|
||||
from: 'sender@ttltest.com',
|
||||
to: ['recipient1@ttltest.com'],
|
||||
subject: 'TTL Test 1',
|
||||
text: 'First email to test TTL',
|
||||
messageId: 'ttl-test-1@ttltest.com'
|
||||
});
|
||||
|
||||
console.log(' Sending first email...');
|
||||
await smtpClient.sendMail(email1);
|
||||
console.log(' ✓ First email sent');
|
||||
|
||||
console.log(' Waiting 500ms (within TTL)...');
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
const email2 = new Email({
|
||||
from: 'sender@ttltest.com',
|
||||
to: ['recipient2@ttltest.com'],
|
||||
subject: 'TTL Test 2',
|
||||
text: 'Second email within TTL',
|
||||
messageId: 'ttl-test-2@ttltest.com'
|
||||
});
|
||||
|
||||
console.log(' Sending second email (should use cache)...');
|
||||
await smtpClient.sendMail(email2);
|
||||
console.log(' ✓ Second email sent');
|
||||
|
||||
console.log(' Waiting 1000ms (TTL expiry)...');
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
const email3 = new Email({
|
||||
from: 'sender@ttltest.com',
|
||||
to: ['recipient3@ttltest.com'],
|
||||
subject: 'TTL Test 3',
|
||||
text: 'Third email after TTL expiry',
|
||||
messageId: 'ttl-test-3@ttltest.com'
|
||||
});
|
||||
|
||||
console.log(' Sending third email (should trigger new lookup)...');
|
||||
await smtpClient.sendMail(email3);
|
||||
console.log(' ✓ Third email sent');
|
||||
|
||||
console.log(` Total DNS lookups: ${dnsLookupCount}`);
|
||||
console.log(` Expected pattern: Initial lookup -> Cache hit -> TTL refresh`);
|
||||
|
||||
if (lookupTimes.length >= 2) {
|
||||
const timeBetweenLookups = lookupTimes[1] - lookupTimes[0];
|
||||
console.log(` Time between DNS lookups: ${timeBetweenLookups}ms`);
|
||||
console.log(` TTL behavior: ${timeBetweenLookups > 1000 ? 'Correct' : 'Needs review'}`);
|
||||
}
|
||||
|
||||
smtpClient.close();
|
||||
} finally {
|
||||
require('dns').lookup = originalLookup;
|
||||
testServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Scenario 4: DNS Cache Memory Management
|
||||
await test.test('Scenario 4: DNS Cache Memory Management', async () => {
|
||||
console.log('\n💾 Testing DNS cache memory management and cleanup...');
|
||||
|
||||
let dnsLookupCount = 0;
|
||||
const dnsCache = new Map();
|
||||
let cacheSize = 0;
|
||||
const originalLookup = require('dns').lookup;
|
||||
|
||||
require('dns').lookup = (hostname: string, options: any, callback: any) => {
|
||||
dnsLookupCount++;
|
||||
|
||||
if (!dnsCache.has(hostname)) {
|
||||
dnsCache.set(hostname, {
|
||||
ip: '127.0.0.1',
|
||||
timestamp: Date.now(),
|
||||
hits: 1
|
||||
});
|
||||
cacheSize++;
|
||||
console.log(` [DNS] New cache entry for ${hostname} (cache size: ${cacheSize})`);
|
||||
} else {
|
||||
const entry = dnsCache.get(hostname);
|
||||
entry.hits++;
|
||||
console.log(` [DNS] Cache hit for ${hostname} (hits: ${entry.hits})`);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
callback(null, '127.0.0.1', 4);
|
||||
}, 30);
|
||||
};
|
||||
|
||||
const testServer = await createTestServer({
|
||||
responseDelay: 10
|
||||
});
|
||||
|
||||
try {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 2,
|
||||
dnsCache: true,
|
||||
dnsCacheTtl: 2000,
|
||||
dnsCacheSize: 5 // Small cache size for testing
|
||||
});
|
||||
|
||||
console.log(' Creating emails to many domains to test cache limits...');
|
||||
const domains = [];
|
||||
for (let i = 0; i < 8; i++) {
|
||||
domains.push(`domain${i}.example.com`);
|
||||
}
|
||||
|
||||
console.log(' Sending emails to test cache memory management...');
|
||||
for (let i = 0; i < domains.length; i++) {
|
||||
const email = new Email({
|
||||
from: `sender@${domains[i]}`,
|
||||
to: [`recipient@${domains[i]}`],
|
||||
subject: `Cache Memory Test ${i + 1}`,
|
||||
text: `Testing cache for ${domains[i]}`,
|
||||
messageId: `cache-mem-${i}@${domains[i]}`
|
||||
});
|
||||
|
||||
try {
|
||||
await smtpClient.sendMail(email);
|
||||
console.log(` ✓ Email ${i + 1} to ${domains[i]} sent`);
|
||||
} catch (error) {
|
||||
console.log(` ✗ Email ${i + 1} failed: ${error.message}`);
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
console.log(' Testing cache hit rates by resending to same domains...');
|
||||
let cacheHits = 0;
|
||||
const initialLookups = dnsLookupCount;
|
||||
|
||||
for (let i = 0; i < 4; i++) { // Resend to first 4 domains
|
||||
const email = new Email({
|
||||
from: `sender@${domains[i]}`,
|
||||
to: [`recipient2@${domains[i]}`],
|
||||
subject: `Cache Hit Test ${i + 1}`,
|
||||
text: `Testing cache hits for ${domains[i]}`,
|
||||
messageId: `cache-hit-${i}@${domains[i]}`
|
||||
});
|
||||
|
||||
const beforeLookups = dnsLookupCount;
|
||||
try {
|
||||
await smtpClient.sendMail(email);
|
||||
const afterLookups = dnsLookupCount;
|
||||
if (afterLookups === beforeLookups) {
|
||||
cacheHits++;
|
||||
console.log(` ✓ Cache hit for ${domains[i]}`);
|
||||
} else {
|
||||
console.log(` ⚡ Cache miss for ${domains[i]}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(` ✗ Email failed: ${error.message}`);
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
}
|
||||
|
||||
const finalLookups = dnsLookupCount;
|
||||
console.log(` Total DNS lookups: ${finalLookups}`);
|
||||
console.log(` Unique domains tested: ${domains.length}`);
|
||||
console.log(` Cache entries created: ${cacheSize}`);
|
||||
console.log(` Cache hits on retests: ${cacheHits}/4`);
|
||||
console.log(` Cache efficiency: ${((cacheHits / 4) * 100).toFixed(1)}%`);
|
||||
console.log(` Memory management: ${cacheSize <= 5 ? 'Within limits' : 'Exceeded limits'}`);
|
||||
|
||||
smtpClient.close();
|
||||
} finally {
|
||||
require('dns').lookup = originalLookup;
|
||||
testServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Scenario 5: DNS Resolution Performance Impact
|
||||
await test.test('Scenario 5: DNS Resolution Performance Impact', async () => {
|
||||
console.log('\n⚡ Testing DNS resolution performance impact on email sending...');
|
||||
|
||||
let slowLookupCount = 0;
|
||||
let fastLookupCount = 0;
|
||||
const originalLookup = require('dns').lookup;
|
||||
|
||||
// First test: Slow DNS responses
|
||||
console.log(' Phase 1: Testing with slow DNS responses (200ms delay)...');
|
||||
require('dns').lookup = (hostname: string, options: any, callback: any) => {
|
||||
slowLookupCount++;
|
||||
console.log(` [DNS-SLOW] Lookup ${slowLookupCount} for: ${hostname}`);
|
||||
|
||||
setTimeout(() => {
|
||||
callback(null, '127.0.0.1', 4);
|
||||
}, 200); // 200ms delay
|
||||
};
|
||||
|
||||
const testServer1 = await createTestServer({
|
||||
responseDelay: 10
|
||||
});
|
||||
|
||||
const smtpClient1 = createSmtpClient({
|
||||
host: testServer1.hostname,
|
||||
port: testServer1.port,
|
||||
secure: false,
|
||||
pool: false, // No pooling to force DNS lookups
|
||||
dnsCache: false
|
||||
});
|
||||
|
||||
const emails1 = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
emails1.push(new Email({
|
||||
from: 'sender@slow.example.com',
|
||||
to: [`recipient${i}@slow.example.com`],
|
||||
subject: `Slow DNS Test ${i + 1}`,
|
||||
text: `Testing slow DNS impact ${i + 1}`,
|
||||
messageId: `slow-dns-${i + 1}@slow.example.com`
|
||||
}));
|
||||
}
|
||||
|
||||
const slowStartTime = Date.now();
|
||||
const slowResults = [];
|
||||
for (const email of emails1) {
|
||||
try {
|
||||
const result = await smtpClient1.sendMail(email);
|
||||
slowResults.push({ success: true });
|
||||
console.log(` ✓ Slow DNS email sent`);
|
||||
} catch (error) {
|
||||
slowResults.push({ success: false });
|
||||
console.log(` ✗ Slow DNS email failed`);
|
||||
}
|
||||
}
|
||||
const slowEndTime = Date.now();
|
||||
const slowTotalTime = slowEndTime - slowStartTime;
|
||||
|
||||
smtpClient1.close();
|
||||
testServer1.close();
|
||||
|
||||
// Second test: Fast DNS responses with caching
|
||||
console.log(' Phase 2: Testing with fast DNS responses and caching...');
|
||||
require('dns').lookup = (hostname: string, options: any, callback: any) => {
|
||||
fastLookupCount++;
|
||||
console.log(` [DNS-FAST] Lookup ${fastLookupCount} for: ${hostname}`);
|
||||
|
||||
setTimeout(() => {
|
||||
callback(null, '127.0.0.1', 4);
|
||||
}, 5); // 5ms delay
|
||||
};
|
||||
|
||||
const testServer2 = await createTestServer({
|
||||
responseDelay: 10
|
||||
});
|
||||
|
||||
const smtpClient2 = createSmtpClient({
|
||||
host: testServer2.hostname,
|
||||
port: testServer2.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
dnsCache: true,
|
||||
dnsCacheTtl: 5000
|
||||
});
|
||||
|
||||
const emails2 = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
emails2.push(new Email({
|
||||
from: 'sender@fast.example.com',
|
||||
to: [`recipient${i}@fast.example.com`],
|
||||
subject: `Fast DNS Test ${i + 1}`,
|
||||
text: `Testing fast DNS impact ${i + 1}`,
|
||||
messageId: `fast-dns-${i + 1}@fast.example.com`
|
||||
}));
|
||||
}
|
||||
|
||||
const fastStartTime = Date.now();
|
||||
const fastResults = [];
|
||||
for (const email of emails2) {
|
||||
try {
|
||||
const result = await smtpClient2.sendMail(email);
|
||||
fastResults.push({ success: true });
|
||||
console.log(` ✓ Fast DNS email sent`);
|
||||
} catch (error) {
|
||||
fastResults.push({ success: false });
|
||||
console.log(` ✗ Fast DNS email failed`);
|
||||
}
|
||||
}
|
||||
const fastEndTime = Date.now();
|
||||
const fastTotalTime = fastEndTime - fastStartTime;
|
||||
|
||||
smtpClient2.close();
|
||||
testServer2.close();
|
||||
|
||||
// Performance comparison
|
||||
const slowSuccess = slowResults.filter(r => r.success).length;
|
||||
const fastSuccess = fastResults.filter(r => r.success).length;
|
||||
const performanceImprovement = ((slowTotalTime - fastTotalTime) / slowTotalTime) * 100;
|
||||
|
||||
console.log(` Slow DNS Results: ${slowTotalTime}ms, ${slowSuccess}/${emails1.length} successful`);
|
||||
console.log(` Fast DNS Results: ${fastTotalTime}ms, ${fastSuccess}/${emails2.length} successful`);
|
||||
console.log(` Performance improvement: ${performanceImprovement.toFixed(1)}%`);
|
||||
console.log(` DNS lookups - Slow: ${slowLookupCount}, Fast: ${fastLookupCount}`);
|
||||
console.log(` Caching efficiency: ${fastLookupCount < slowLookupCount ? 'Effective' : 'Needs improvement'}`);
|
||||
|
||||
// Restore original DNS lookup
|
||||
require('dns').lookup = originalLookup;
|
||||
});
|
||||
|
||||
console.log('\n✅ CPERF-08: DNS Caching Efficiency Performance Tests completed');
|
||||
console.log('🌐 All DNS caching scenarios tested successfully');
|
||||
});
|
@ -0,0 +1,560 @@
|
||||
import { test } from '@git.zone/tstest/tapbundle';
|
||||
import { createTestServer, createSmtpClient } from '../../helpers/utils.js';
|
||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
test('CREL-03: Queue Persistence Reliability Tests', async () => {
|
||||
console.log('\n💾 Testing SMTP Client Queue Persistence Reliability');
|
||||
console.log('=' .repeat(60));
|
||||
|
||||
const tempDir = path.join(process.cwd(), '.nogit', 'test-queue-persistence');
|
||||
|
||||
// Ensure test directory exists
|
||||
if (!fs.existsSync(tempDir)) {
|
||||
fs.mkdirSync(tempDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Scenario 1: Queue State Persistence Across Restarts
|
||||
await test.test('Scenario 1: Queue State Persistence Across Restarts', async () => {
|
||||
console.log('\n🔄 Testing queue state persistence across client restarts...');
|
||||
|
||||
let messageCount = 0;
|
||||
const processedMessages: string[] = [];
|
||||
|
||||
const testServer = await createTestServer({
|
||||
responseDelay: 100,
|
||||
onData: (data: string) => {
|
||||
if (data.includes('Message-ID:')) {
|
||||
const messageIdMatch = data.match(/Message-ID:\s*<([^>]+)>/);
|
||||
if (messageIdMatch) {
|
||||
messageCount++;
|
||||
processedMessages.push(messageIdMatch[1]);
|
||||
console.log(` [Server] Processed message ${messageCount}: ${messageIdMatch[1]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
console.log(' Phase 1: Creating first client instance with queue...');
|
||||
const queueFile = path.join(tempDir, 'test-queue-1.json');
|
||||
|
||||
// Remove any existing queue file
|
||||
if (fs.existsSync(queueFile)) {
|
||||
fs.unlinkSync(queueFile);
|
||||
}
|
||||
|
||||
const smtpClient1 = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 1,
|
||||
maxMessages: 100,
|
||||
// Queue persistence settings
|
||||
queuePath: queueFile,
|
||||
persistQueue: true,
|
||||
retryDelay: 200,
|
||||
retries: 3
|
||||
});
|
||||
|
||||
console.log(' Creating emails for persistence test...');
|
||||
const emails = [];
|
||||
for (let i = 0; i < 6; i++) {
|
||||
emails.push(new Email({
|
||||
from: 'sender@persistence.test',
|
||||
to: [`recipient${i}@persistence.test`],
|
||||
subject: `Persistence Test Email ${i + 1}`,
|
||||
text: `Testing queue persistence, email ${i + 1}`,
|
||||
messageId: `persist-${i + 1}@persistence.test`
|
||||
}));
|
||||
}
|
||||
|
||||
console.log(' Sending emails to build up queue...');
|
||||
const sendPromises = emails.map((email, index) => {
|
||||
return smtpClient1.sendMail(email).then(result => {
|
||||
console.log(` 📤 Email ${index + 1} queued successfully`);
|
||||
return { success: true, result, index };
|
||||
}).catch(error => {
|
||||
console.log(` ❌ Email ${index + 1} failed: ${error.message}`);
|
||||
return { success: false, error, index };
|
||||
});
|
||||
});
|
||||
|
||||
// Allow some emails to be queued
|
||||
await new Promise(resolve => setTimeout(resolve, 150));
|
||||
|
||||
console.log(' Phase 2: Simulating client restart by closing first instance...');
|
||||
smtpClient1.close();
|
||||
|
||||
// Wait for queue file to be written
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
|
||||
console.log(' Checking queue persistence file...');
|
||||
const queueExists = fs.existsSync(queueFile);
|
||||
console.log(` Queue file exists: ${queueExists}`);
|
||||
|
||||
if (queueExists) {
|
||||
const queueData = fs.readFileSync(queueFile, 'utf8');
|
||||
console.log(` Queue file size: ${queueData.length} bytes`);
|
||||
|
||||
try {
|
||||
const parsedQueue = JSON.parse(queueData);
|
||||
console.log(` Persisted queue items: ${Array.isArray(parsedQueue) ? parsedQueue.length : 'Unknown format'}`);
|
||||
} catch (parseError) {
|
||||
console.log(` Queue file parse error: ${parseError.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(' Phase 3: Creating second client instance to resume queue...');
|
||||
const smtpClient2 = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 1,
|
||||
maxMessages: 100,
|
||||
queuePath: queueFile,
|
||||
persistQueue: true,
|
||||
resumeQueue: true, // Resume from persisted queue
|
||||
retryDelay: 200,
|
||||
retries: 3
|
||||
});
|
||||
|
||||
console.log(' Waiting for queue resumption and processing...');
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Try to resolve original promises or create new ones for remaining emails
|
||||
try {
|
||||
await Promise.allSettled(sendPromises);
|
||||
} catch (error) {
|
||||
console.log(` Send promises resolution: ${error.message}`);
|
||||
}
|
||||
|
||||
console.log(' Phase 4: Verifying queue recovery results...');
|
||||
console.log(` Total messages processed by server: ${messageCount}`);
|
||||
console.log(` Processed message IDs: ${processedMessages.join(', ')}`);
|
||||
console.log(` Expected emails: ${emails.length}`);
|
||||
console.log(` Queue persistence success: ${messageCount >= emails.length - 2 ? 'Good' : 'Partial'}`);
|
||||
|
||||
smtpClient2.close();
|
||||
|
||||
// Cleanup
|
||||
if (fs.existsSync(queueFile)) {
|
||||
fs.unlinkSync(queueFile);
|
||||
}
|
||||
|
||||
} finally {
|
||||
testServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Scenario 2: Queue Corruption Recovery
|
||||
await test.test('Scenario 2: Queue Corruption Recovery', async () => {
|
||||
console.log('\n🛠️ Testing queue corruption recovery mechanisms...');
|
||||
|
||||
const testServer = await createTestServer({
|
||||
responseDelay: 50,
|
||||
onConnect: () => {
|
||||
console.log(' [Server] Connection established for corruption test');
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const queueFile = path.join(tempDir, 'corrupted-queue.json');
|
||||
|
||||
console.log(' Creating corrupted queue file...');
|
||||
// Create a corrupted JSON file
|
||||
fs.writeFileSync(queueFile, '{"invalid": json, "missing_bracket": true');
|
||||
console.log(' Corrupted queue file created');
|
||||
|
||||
console.log(' Testing client behavior with corrupted queue...');
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 1,
|
||||
queuePath: queueFile,
|
||||
persistQueue: true,
|
||||
resumeQueue: true,
|
||||
corruptionRecovery: true // Enable corruption recovery
|
||||
});
|
||||
|
||||
const email = new Email({
|
||||
from: 'sender@corruption.test',
|
||||
to: ['recipient@corruption.test'],
|
||||
subject: 'Corruption Recovery Test',
|
||||
text: 'Testing recovery from corrupted queue',
|
||||
messageId: 'corruption-test@corruption.test'
|
||||
});
|
||||
|
||||
console.log(' Sending email with corrupted queue present...');
|
||||
try {
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(' ✓ Email sent successfully despite corrupted queue');
|
||||
console.log(` Message ID: ${result.messageId}`);
|
||||
} catch (error) {
|
||||
console.log(' ✗ Email failed to send');
|
||||
console.log(` Error: ${error.message}`);
|
||||
}
|
||||
|
||||
console.log(' Checking queue file after corruption recovery...');
|
||||
if (fs.existsSync(queueFile)) {
|
||||
try {
|
||||
const recoveredData = fs.readFileSync(queueFile, 'utf8');
|
||||
JSON.parse(recoveredData); // Try to parse
|
||||
console.log(' ✓ Queue file recovered and is valid JSON');
|
||||
} catch (parseError) {
|
||||
console.log(' ⚠️ Queue file still corrupted or replaced');
|
||||
}
|
||||
} else {
|
||||
console.log(' ℹ️ Corrupted queue file was removed/replaced');
|
||||
}
|
||||
|
||||
smtpClient.close();
|
||||
|
||||
// Cleanup
|
||||
if (fs.existsSync(queueFile)) {
|
||||
fs.unlinkSync(queueFile);
|
||||
}
|
||||
|
||||
} finally {
|
||||
testServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Scenario 3: Queue Size Limits and Rotation
|
||||
await test.test('Scenario 3: Queue Size Limits and Rotation', async () => {
|
||||
console.log('\n📏 Testing queue size limits and rotation...');
|
||||
|
||||
const testServer = await createTestServer({
|
||||
responseDelay: 200, // Slow server to build up queue
|
||||
onConnect: () => {
|
||||
console.log(' [Server] Slow connection established');
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const queueFile = path.join(tempDir, 'size-limit-queue.json');
|
||||
|
||||
if (fs.existsSync(queueFile)) {
|
||||
fs.unlinkSync(queueFile);
|
||||
}
|
||||
|
||||
console.log(' Creating client with queue size limits...');
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 1,
|
||||
maxMessages: 5,
|
||||
queuePath: queueFile,
|
||||
persistQueue: true,
|
||||
maxQueueSize: 1024, // 1KB queue size limit
|
||||
queueRotation: true
|
||||
});
|
||||
|
||||
console.log(' Creating many emails to test queue limits...');
|
||||
const emails = [];
|
||||
for (let i = 0; i < 15; i++) {
|
||||
emails.push(new Email({
|
||||
from: 'sender@sizelimit.test',
|
||||
to: [`recipient${i}@sizelimit.test`],
|
||||
subject: `Size Limit Test Email ${i + 1}`,
|
||||
text: `Testing queue size limits with a longer message body that contains more text to increase the queue file size. This is email number ${i + 1} in the sequence of emails designed to test queue rotation and size management. Adding more content here to make the queue file larger.`,
|
||||
messageId: `sizelimit-${i + 1}@sizelimit.test`
|
||||
}));
|
||||
}
|
||||
|
||||
let successCount = 0;
|
||||
let rejectCount = 0;
|
||||
|
||||
console.log(' Sending emails rapidly to test queue limits...');
|
||||
for (let i = 0; i < emails.length; i++) {
|
||||
try {
|
||||
const promise = smtpClient.sendMail(emails[i]);
|
||||
console.log(` 📤 Email ${i + 1} queued`);
|
||||
|
||||
// Don't wait for completion, just queue them rapidly
|
||||
promise.then(() => {
|
||||
successCount++;
|
||||
console.log(` ✓ Email ${i + 1} sent successfully`);
|
||||
}).catch((error) => {
|
||||
rejectCount++;
|
||||
console.log(` ❌ Email ${i + 1} rejected: ${error.message}`);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
rejectCount++;
|
||||
console.log(` ❌ Email ${i + 1} immediate rejection: ${error.message}`);
|
||||
}
|
||||
|
||||
// Check queue file size periodically
|
||||
if (i % 5 === 0 && fs.existsSync(queueFile)) {
|
||||
const stats = fs.statSync(queueFile);
|
||||
console.log(` Queue file size: ${stats.size} bytes`);
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 20));
|
||||
}
|
||||
|
||||
console.log(' Waiting for queue processing to complete...');
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
// Final queue file check
|
||||
if (fs.existsSync(queueFile)) {
|
||||
const finalStats = fs.statSync(queueFile);
|
||||
console.log(` Final queue file size: ${finalStats.size} bytes`);
|
||||
console.log(` Size limit respected: ${finalStats.size <= 1024 ? 'Yes' : 'No'}`);
|
||||
}
|
||||
|
||||
console.log(` Success count: ${successCount}`);
|
||||
console.log(` Reject count: ${rejectCount}`);
|
||||
console.log(` Total processed: ${successCount + rejectCount}/${emails.length}`);
|
||||
console.log(` Queue management: ${rejectCount > 0 ? 'Enforced limits' : 'No limits hit'}`);
|
||||
|
||||
smtpClient.close();
|
||||
|
||||
if (fs.existsSync(queueFile)) {
|
||||
fs.unlinkSync(queueFile);
|
||||
}
|
||||
|
||||
} finally {
|
||||
testServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Scenario 4: Concurrent Queue Access Safety
|
||||
await test.test('Scenario 4: Concurrent Queue Access Safety', async () => {
|
||||
console.log('\n🔒 Testing concurrent queue access safety...');
|
||||
|
||||
const testServer = await createTestServer({
|
||||
responseDelay: 30
|
||||
});
|
||||
|
||||
try {
|
||||
const queueFile = path.join(tempDir, 'concurrent-queue.json');
|
||||
|
||||
if (fs.existsSync(queueFile)) {
|
||||
fs.unlinkSync(queueFile);
|
||||
}
|
||||
|
||||
console.log(' Creating multiple client instances sharing same queue file...');
|
||||
|
||||
const clients = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
clients.push(createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 1,
|
||||
queuePath: queueFile,
|
||||
persistQueue: true,
|
||||
queueLocking: true, // Enable file locking
|
||||
lockTimeout: 1000
|
||||
}));
|
||||
}
|
||||
|
||||
console.log(' Creating emails for concurrent access test...');
|
||||
const allEmails = [];
|
||||
for (let clientIndex = 0; clientIndex < clients.length; clientIndex++) {
|
||||
for (let emailIndex = 0; emailIndex < 4; emailIndex++) {
|
||||
allEmails.push({
|
||||
client: clients[clientIndex],
|
||||
email: new Email({
|
||||
from: `sender${clientIndex}@concurrent.test`,
|
||||
to: [`recipient${clientIndex}-${emailIndex}@concurrent.test`],
|
||||
subject: `Concurrent Test Client ${clientIndex + 1} Email ${emailIndex + 1}`,
|
||||
text: `Testing concurrent queue access from client ${clientIndex + 1}`,
|
||||
messageId: `concurrent-${clientIndex}-${emailIndex}@concurrent.test`
|
||||
}),
|
||||
clientId: clientIndex,
|
||||
emailId: emailIndex
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(' Sending emails concurrently from multiple clients...');
|
||||
const startTime = Date.now();
|
||||
|
||||
const promises = allEmails.map(({ client, email, clientId, emailId }) => {
|
||||
return client.sendMail(email).then(result => {
|
||||
console.log(` ✓ Client ${clientId + 1} Email ${emailId + 1} sent`);
|
||||
return { success: true, clientId, emailId, result };
|
||||
}).catch(error => {
|
||||
console.log(` ✗ Client ${clientId + 1} Email ${emailId + 1} failed: ${error.message}`);
|
||||
return { success: false, clientId, emailId, error };
|
||||
});
|
||||
});
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
const endTime = Date.now();
|
||||
|
||||
const successful = results.filter(r => r.success).length;
|
||||
const failed = results.filter(r => !r.success).length;
|
||||
|
||||
console.log(` Concurrent operations completed in ${endTime - startTime}ms`);
|
||||
console.log(` Total emails: ${allEmails.length}`);
|
||||
console.log(` Successful: ${successful}, Failed: ${failed}`);
|
||||
console.log(` Success rate: ${((successful / allEmails.length) * 100).toFixed(1)}%`);
|
||||
|
||||
// Check queue file integrity
|
||||
if (fs.existsSync(queueFile)) {
|
||||
try {
|
||||
const queueData = fs.readFileSync(queueFile, 'utf8');
|
||||
JSON.parse(queueData);
|
||||
console.log(' ✓ Queue file integrity maintained during concurrent access');
|
||||
} catch (error) {
|
||||
console.log(' ❌ Queue file corrupted during concurrent access');
|
||||
}
|
||||
}
|
||||
|
||||
// Close all clients
|
||||
for (const client of clients) {
|
||||
client.close();
|
||||
}
|
||||
|
||||
if (fs.existsSync(queueFile)) {
|
||||
fs.unlinkSync(queueFile);
|
||||
}
|
||||
|
||||
} finally {
|
||||
testServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Scenario 5: Queue Data Integrity and Validation
|
||||
await test.test('Scenario 5: Queue Data Integrity and Validation', async () => {
|
||||
console.log('\n🔍 Testing queue data integrity and validation...');
|
||||
|
||||
const testServer = await createTestServer({
|
||||
responseDelay: 40,
|
||||
onData: (data: string) => {
|
||||
if (data.includes('Subject: Integrity Test')) {
|
||||
console.log(' [Server] Received integrity test email');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const queueFile = path.join(tempDir, 'integrity-queue.json');
|
||||
|
||||
if (fs.existsSync(queueFile)) {
|
||||
fs.unlinkSync(queueFile);
|
||||
}
|
||||
|
||||
console.log(' Creating client with queue integrity checking...');
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 1,
|
||||
queuePath: queueFile,
|
||||
persistQueue: true,
|
||||
integrityChecks: true,
|
||||
checksumValidation: true
|
||||
});
|
||||
|
||||
console.log(' Creating test emails with various content types...');
|
||||
const emails = [
|
||||
new Email({
|
||||
from: 'sender@integrity.test',
|
||||
to: ['recipient1@integrity.test'],
|
||||
subject: 'Integrity Test - Plain Text',
|
||||
text: 'Plain text email for integrity testing',
|
||||
messageId: 'integrity-plain@integrity.test'
|
||||
}),
|
||||
new Email({
|
||||
from: 'sender@integrity.test',
|
||||
to: ['recipient2@integrity.test'],
|
||||
subject: 'Integrity Test - HTML',
|
||||
html: '<h1>HTML Email</h1><p>Testing integrity with HTML content</p>',
|
||||
messageId: 'integrity-html@integrity.test'
|
||||
}),
|
||||
new Email({
|
||||
from: 'sender@integrity.test',
|
||||
to: ['recipient3@integrity.test'],
|
||||
subject: 'Integrity Test - Special Characters',
|
||||
text: 'Testing with special characters: ñáéíóú, 中文, العربية, русский',
|
||||
messageId: 'integrity-special@integrity.test'
|
||||
})
|
||||
];
|
||||
|
||||
console.log(' Sending emails and monitoring queue integrity...');
|
||||
for (let i = 0; i < emails.length; i++) {
|
||||
try {
|
||||
const result = await smtpClient.sendMail(emails[i]);
|
||||
console.log(` ✓ Email ${i + 1} sent and queued`);
|
||||
|
||||
// Check queue file after each email
|
||||
if (fs.existsSync(queueFile)) {
|
||||
const queueData = fs.readFileSync(queueFile, 'utf8');
|
||||
try {
|
||||
const parsed = JSON.parse(queueData);
|
||||
console.log(` 📊 Queue contains ${Array.isArray(parsed) ? parsed.length : 'unknown'} items`);
|
||||
} catch (parseError) {
|
||||
console.log(' ❌ Queue file parsing failed - integrity compromised');
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log(` ✗ Email ${i + 1} failed: ${error.message}`);
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
console.log(' Performing final integrity validation...');
|
||||
|
||||
// Manual integrity check
|
||||
if (fs.existsSync(queueFile)) {
|
||||
const fileContent = fs.readFileSync(queueFile, 'utf8');
|
||||
const fileSize = fileContent.length;
|
||||
|
||||
try {
|
||||
const queueData = JSON.parse(fileContent);
|
||||
const hasValidStructure = Array.isArray(queueData) || typeof queueData === 'object';
|
||||
|
||||
console.log(` Queue file size: ${fileSize} bytes`);
|
||||
console.log(` Valid JSON structure: ${hasValidStructure ? 'Yes' : 'No'}`);
|
||||
console.log(` Data integrity: ${hasValidStructure && fileSize > 0 ? 'Maintained' : 'Compromised'}`);
|
||||
|
||||
} catch (error) {
|
||||
console.log(' ❌ Final integrity check failed: Invalid JSON');
|
||||
}
|
||||
} else {
|
||||
console.log(' ℹ️ Queue file not found (may have been processed completely)');
|
||||
}
|
||||
|
||||
smtpClient.close();
|
||||
|
||||
if (fs.existsSync(queueFile)) {
|
||||
fs.unlinkSync(queueFile);
|
||||
}
|
||||
|
||||
} finally {
|
||||
testServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Cleanup test directory
|
||||
try {
|
||||
if (fs.existsSync(tempDir)) {
|
||||
const files = fs.readdirSync(tempDir);
|
||||
for (const file of files) {
|
||||
fs.unlinkSync(path.join(tempDir, file));
|
||||
}
|
||||
fs.rmdirSync(tempDir);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(` Warning: Could not clean up test directory: ${error.message}`);
|
||||
}
|
||||
|
||||
console.log('\n✅ CREL-03: Queue Persistence Reliability Tests completed');
|
||||
console.log('💾 All queue persistence scenarios tested successfully');
|
||||
});
|
572
test/suite/smtpclient_reliability/test.crel-04.crash-recovery.ts
Normal file
572
test/suite/smtpclient_reliability/test.crel-04.crash-recovery.ts
Normal file
@ -0,0 +1,572 @@
|
||||
import { test } from '@git.zone/tstest/tapbundle';
|
||||
import { createTestServer, createSmtpClient } from '../../helpers/utils.js';
|
||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as child_process from 'child_process';
|
||||
|
||||
test('CREL-04: Crash Recovery Reliability Tests', async () => {
|
||||
console.log('\n💥 Testing SMTP Client Crash Recovery Reliability');
|
||||
console.log('=' .repeat(60));
|
||||
|
||||
const tempDir = path.join(process.cwd(), '.nogit', 'test-crash-recovery');
|
||||
|
||||
// Ensure test directory exists
|
||||
if (!fs.existsSync(tempDir)) {
|
||||
fs.mkdirSync(tempDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Scenario 1: Graceful Recovery from Connection Drops
|
||||
await test.test('Scenario 1: Graceful Recovery from Connection Drops', async () => {
|
||||
console.log('\n🔌 Testing recovery from sudden connection drops...');
|
||||
|
||||
let connectionCount = 0;
|
||||
let dropConnections = false;
|
||||
const testServer = await createTestServer({
|
||||
responseDelay: 50,
|
||||
onConnect: (socket: any) => {
|
||||
connectionCount++;
|
||||
console.log(` [Server] Connection ${connectionCount} established`);
|
||||
|
||||
if (dropConnections && connectionCount > 2) {
|
||||
console.log(` [Server] Simulating connection drop for connection ${connectionCount}`);
|
||||
setTimeout(() => {
|
||||
socket.destroy();
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
onData: (data: string) => {
|
||||
if (data.includes('Subject: Drop Recovery Test')) {
|
||||
console.log(' [Server] Received drop recovery email');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
console.log(' Creating SMTP client with crash recovery settings...');
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 2,
|
||||
maxMessages: 50,
|
||||
// Recovery settings
|
||||
retryDelay: 200,
|
||||
retries: 5,
|
||||
reconnectOnFailure: true,
|
||||
connectionTimeout: 1000,
|
||||
recoveryMode: 'aggressive'
|
||||
});
|
||||
|
||||
const emails = [];
|
||||
for (let i = 0; i < 8; i++) {
|
||||
emails.push(new Email({
|
||||
from: 'sender@crashtest.example',
|
||||
to: [`recipient${i}@crashtest.example`],
|
||||
subject: `Drop Recovery Test ${i + 1}`,
|
||||
text: `Testing connection drop recovery, email ${i + 1}`,
|
||||
messageId: `drop-recovery-${i + 1}@crashtest.example`
|
||||
}));
|
||||
}
|
||||
|
||||
console.log(' Phase 1: Sending initial emails (connections should succeed)...');
|
||||
const results1 = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
try {
|
||||
const result = await smtpClient.sendMail(emails[i]);
|
||||
results1.push({ success: true, index: i });
|
||||
console.log(` ✓ Email ${i + 1} sent successfully`);
|
||||
} catch (error) {
|
||||
results1.push({ success: false, index: i, error });
|
||||
console.log(` ✗ Email ${i + 1} failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(' Phase 2: Enabling connection drops...');
|
||||
dropConnections = true;
|
||||
|
||||
console.log(' Sending emails during connection instability...');
|
||||
const results2 = [];
|
||||
const promises = emails.slice(3).map((email, index) => {
|
||||
const actualIndex = index + 3;
|
||||
return smtpClient.sendMail(email).then(result => {
|
||||
console.log(` ✓ Email ${actualIndex + 1} recovered and sent`);
|
||||
return { success: true, index: actualIndex, result };
|
||||
}).catch(error => {
|
||||
console.log(` ✗ Email ${actualIndex + 1} failed permanently: ${error.message}`);
|
||||
return { success: false, index: actualIndex, error };
|
||||
});
|
||||
});
|
||||
|
||||
const results2Resolved = await Promise.all(promises);
|
||||
results2.push(...results2Resolved);
|
||||
|
||||
const totalSuccessful = [...results1, ...results2].filter(r => r.success).length;
|
||||
const totalFailed = [...results1, ...results2].filter(r => !r.success).length;
|
||||
|
||||
console.log(` Connection attempts: ${connectionCount}`);
|
||||
console.log(` Emails sent successfully: ${totalSuccessful}/${emails.length}`);
|
||||
console.log(` Failed emails: ${totalFailed}`);
|
||||
console.log(` Recovery effectiveness: ${((totalSuccessful / emails.length) * 100).toFixed(1)}%`);
|
||||
|
||||
smtpClient.close();
|
||||
} finally {
|
||||
testServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Scenario 2: Recovery from Server Process Crashes
|
||||
await test.test('Scenario 2: Recovery from Server Process Crashes', async () => {
|
||||
console.log('\n💀 Testing recovery from server process crashes...');
|
||||
|
||||
// Start first server instance
|
||||
let server1 = await createTestServer({
|
||||
responseDelay: 30,
|
||||
onConnect: () => {
|
||||
console.log(' [Server1] Connection established');
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
console.log(' Creating client with crash recovery capabilities...');
|
||||
const smtpClient = createSmtpClient({
|
||||
host: server1.hostname,
|
||||
port: server1.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 1,
|
||||
retryDelay: 500,
|
||||
retries: 10,
|
||||
reconnectOnFailure: true,
|
||||
serverCrashRecovery: true
|
||||
});
|
||||
|
||||
const emails = [];
|
||||
for (let i = 0; i < 6; i++) {
|
||||
emails.push(new Email({
|
||||
from: 'sender@servercrash.test',
|
||||
to: [`recipient${i}@servercrash.test`],
|
||||
subject: `Server Crash Recovery ${i + 1}`,
|
||||
text: `Testing server crash recovery, email ${i + 1}`,
|
||||
messageId: `server-crash-${i + 1}@servercrash.test`
|
||||
}));
|
||||
}
|
||||
|
||||
console.log(' Sending first batch of emails...');
|
||||
const result1 = await smtpClient.sendMail(emails[0]);
|
||||
console.log(' ✓ Email 1 sent successfully');
|
||||
|
||||
const result2 = await smtpClient.sendMail(emails[1]);
|
||||
console.log(' ✓ Email 2 sent successfully');
|
||||
|
||||
console.log(' Simulating server crash by closing server...');
|
||||
server1.close();
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
console.log(' Starting new server instance on same port...');
|
||||
const server2 = await createTestServer({
|
||||
port: server1.port, // Same port
|
||||
responseDelay: 30,
|
||||
onConnect: () => {
|
||||
console.log(' [Server2] Connection established after crash');
|
||||
},
|
||||
onData: (data: string) => {
|
||||
if (data.includes('Subject: Server Crash Recovery')) {
|
||||
console.log(' [Server2] Processing recovery email');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log(' Sending emails after server restart...');
|
||||
const recoveryResults = [];
|
||||
|
||||
for (let i = 2; i < emails.length; i++) {
|
||||
try {
|
||||
const result = await smtpClient.sendMail(emails[i]);
|
||||
recoveryResults.push({ success: true, index: i, result });
|
||||
console.log(` ✓ Email ${i + 1} sent after server recovery`);
|
||||
} catch (error) {
|
||||
recoveryResults.push({ success: false, index: i, error });
|
||||
console.log(` ✗ Email ${i + 1} failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const successfulRecovery = recoveryResults.filter(r => r.success).length;
|
||||
const totalSuccessful = 2 + successfulRecovery; // 2 from before crash + recovery
|
||||
|
||||
console.log(` Pre-crash emails: 2/2 successful`);
|
||||
console.log(` Post-crash emails: ${successfulRecovery}/${recoveryResults.length} successful`);
|
||||
console.log(` Overall success rate: ${((totalSuccessful / emails.length) * 100).toFixed(1)}%`);
|
||||
console.log(` Server crash recovery: ${successfulRecovery > 0 ? 'Successful' : 'Failed'}`);
|
||||
|
||||
smtpClient.close();
|
||||
server2.close();
|
||||
} finally {
|
||||
// Ensure cleanup
|
||||
try {
|
||||
server1.close();
|
||||
} catch (e) { /* Already closed */ }
|
||||
}
|
||||
});
|
||||
|
||||
// Scenario 3: Memory Corruption Recovery
|
||||
await test.test('Scenario 3: Memory Corruption Recovery', async () => {
|
||||
console.log('\n🧠 Testing recovery from memory corruption scenarios...');
|
||||
|
||||
const testServer = await createTestServer({
|
||||
responseDelay: 20,
|
||||
onData: (data: string) => {
|
||||
if (data.includes('Subject: Memory Corruption')) {
|
||||
console.log(' [Server] Processing memory corruption test email');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
console.log(' Creating client with memory protection...');
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 2,
|
||||
memoryProtection: true,
|
||||
corruptionDetection: true,
|
||||
safeMode: true
|
||||
});
|
||||
|
||||
console.log(' Creating emails with potentially problematic content...');
|
||||
const emails = [
|
||||
new Email({
|
||||
from: 'sender@memcorrupt.test',
|
||||
to: ['recipient1@memcorrupt.test'],
|
||||
subject: 'Memory Corruption Test - Normal',
|
||||
text: 'Normal email content',
|
||||
messageId: 'mem-normal@memcorrupt.test'
|
||||
}),
|
||||
new Email({
|
||||
from: 'sender@memcorrupt.test',
|
||||
to: ['recipient2@memcorrupt.test'],
|
||||
subject: 'Memory Corruption Test - Large Buffer',
|
||||
text: 'X'.repeat(100000), // Large content
|
||||
messageId: 'mem-large@memcorrupt.test'
|
||||
}),
|
||||
new Email({
|
||||
from: 'sender@memcorrupt.test',
|
||||
to: ['recipient3@memcorrupt.test'],
|
||||
subject: 'Memory Corruption Test - Binary Data',
|
||||
text: Buffer.from([0x00, 0x01, 0x02, 0xFF, 0xFE, 0xFD]).toString('binary'),
|
||||
messageId: 'mem-binary@memcorrupt.test'
|
||||
}),
|
||||
new Email({
|
||||
from: 'sender@memcorrupt.test',
|
||||
to: ['recipient4@memcorrupt.test'],
|
||||
subject: 'Memory Corruption Test - Unicode',
|
||||
text: '🎭🎪🎨🎯🎲🎸🎺🎻🎼🎵🎶🎷' + '\u0000'.repeat(10) + '🎯🎲',
|
||||
messageId: 'mem-unicode@memcorrupt.test'
|
||||
})
|
||||
];
|
||||
|
||||
console.log(' Sending potentially problematic emails...');
|
||||
const results = [];
|
||||
|
||||
for (let i = 0; i < emails.length; i++) {
|
||||
console.log(` Testing email ${i + 1} (${emails[i].subject.split(' - ')[1]})...`);
|
||||
|
||||
try {
|
||||
// Monitor memory usage before sending
|
||||
const memBefore = process.memoryUsage();
|
||||
console.log(` Memory before: ${Math.round(memBefore.heapUsed / 1024 / 1024)}MB`);
|
||||
|
||||
const result = await smtpClient.sendMail(emails[i]);
|
||||
|
||||
const memAfter = process.memoryUsage();
|
||||
console.log(` Memory after: ${Math.round(memAfter.heapUsed / 1024 / 1024)}MB`);
|
||||
|
||||
const memIncrease = memAfter.heapUsed - memBefore.heapUsed;
|
||||
console.log(` Memory increase: ${Math.round(memIncrease / 1024)}KB`);
|
||||
|
||||
results.push({
|
||||
success: true,
|
||||
index: i,
|
||||
result,
|
||||
memoryIncrease: memIncrease
|
||||
});
|
||||
console.log(` ✓ Email ${i + 1} sent successfully`);
|
||||
|
||||
} catch (error) {
|
||||
results.push({ success: false, index: i, error });
|
||||
console.log(` ✗ Email ${i + 1} failed: ${error.message}`);
|
||||
}
|
||||
|
||||
// Force garbage collection if available
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
const successful = results.filter(r => r.success).length;
|
||||
const totalMemoryIncrease = results.reduce((sum, r) => sum + (r.memoryIncrease || 0), 0);
|
||||
|
||||
console.log(` Memory corruption resistance: ${successful}/${emails.length} emails processed`);
|
||||
console.log(` Total memory increase: ${Math.round(totalMemoryIncrease / 1024)}KB`);
|
||||
console.log(` Memory protection effectiveness: ${((successful / emails.length) * 100).toFixed(1)}%`);
|
||||
|
||||
smtpClient.close();
|
||||
} finally {
|
||||
testServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Scenario 4: State Recovery After Exceptions
|
||||
await test.test('Scenario 4: State Recovery After Exceptions', async () => {
|
||||
console.log('\n⚠️ Testing state recovery after exceptions...');
|
||||
|
||||
let errorInjectionEnabled = false;
|
||||
const testServer = await createTestServer({
|
||||
responseDelay: 30,
|
||||
onData: (data: string, socket: any) => {
|
||||
if (errorInjectionEnabled && data.includes('MAIL FROM')) {
|
||||
console.log(' [Server] Injecting error response');
|
||||
socket.write('550 Simulated server error\r\n');
|
||||
return false; // Prevent normal processing
|
||||
}
|
||||
return true; // Allow normal processing
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
console.log(' Creating client with exception recovery...');
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 1,
|
||||
exceptionRecovery: true,
|
||||
stateValidation: true,
|
||||
retryDelay: 300,
|
||||
retries: 3
|
||||
});
|
||||
|
||||
const emails = [];
|
||||
for (let i = 0; i < 6; i++) {
|
||||
emails.push(new Email({
|
||||
from: 'sender@exception.test',
|
||||
to: [`recipient${i}@exception.test`],
|
||||
subject: `Exception Recovery Test ${i + 1}`,
|
||||
text: `Testing exception recovery, email ${i + 1}`,
|
||||
messageId: `exception-${i + 1}@exception.test`
|
||||
}));
|
||||
}
|
||||
|
||||
console.log(' Phase 1: Sending emails normally...');
|
||||
await smtpClient.sendMail(emails[0]);
|
||||
console.log(' ✓ Email 1 sent successfully');
|
||||
|
||||
await smtpClient.sendMail(emails[1]);
|
||||
console.log(' ✓ Email 2 sent successfully');
|
||||
|
||||
console.log(' Phase 2: Enabling error injection...');
|
||||
errorInjectionEnabled = true;
|
||||
|
||||
console.log(' Sending emails with error injection (should trigger recovery)...');
|
||||
const recoveryResults = [];
|
||||
|
||||
for (let i = 2; i < 4; i++) {
|
||||
try {
|
||||
const result = await smtpClient.sendMail(emails[i]);
|
||||
recoveryResults.push({ success: true, index: i, result });
|
||||
console.log(` ✓ Email ${i + 1} sent despite errors`);
|
||||
} catch (error) {
|
||||
recoveryResults.push({ success: false, index: i, error });
|
||||
console.log(` ✗ Email ${i + 1} failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(' Phase 3: Disabling error injection...');
|
||||
errorInjectionEnabled = false;
|
||||
|
||||
console.log(' Sending final emails (recovery validation)...');
|
||||
for (let i = 4; i < emails.length; i++) {
|
||||
try {
|
||||
const result = await smtpClient.sendMail(emails[i]);
|
||||
recoveryResults.push({ success: true, index: i, result });
|
||||
console.log(` ✓ Email ${i + 1} sent after recovery`);
|
||||
} catch (error) {
|
||||
recoveryResults.push({ success: false, index: i, error });
|
||||
console.log(` ✗ Email ${i + 1} failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const successful = recoveryResults.filter(r => r.success).length;
|
||||
const totalSuccessful = 2 + successful; // 2 initial + recovery phase
|
||||
|
||||
console.log(` Pre-error emails: 2/2 successful`);
|
||||
console.log(` Error phase emails: ${successful}/${recoveryResults.length} successful`);
|
||||
console.log(` Total success rate: ${((totalSuccessful / emails.length) * 100).toFixed(1)}%`);
|
||||
console.log(` Exception recovery: ${successful >= recoveryResults.length - 2 ? 'Effective' : 'Partial'}`);
|
||||
|
||||
smtpClient.close();
|
||||
} finally {
|
||||
testServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Scenario 5: Crash Recovery with Queue Preservation
|
||||
await test.test('Scenario 5: Crash Recovery with Queue Preservation', async () => {
|
||||
console.log('\n💾 Testing crash recovery with queue preservation...');
|
||||
|
||||
const queueFile = path.join(tempDir, 'crash-recovery-queue.json');
|
||||
|
||||
if (fs.existsSync(queueFile)) {
|
||||
fs.unlinkSync(queueFile);
|
||||
}
|
||||
|
||||
const testServer = await createTestServer({
|
||||
responseDelay: 100, // Slow processing to keep items in queue
|
||||
onData: (data: string) => {
|
||||
if (data.includes('Subject: Crash Queue')) {
|
||||
console.log(' [Server] Processing crash recovery email');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
console.log(' Phase 1: Creating client with persistent queue...');
|
||||
const smtpClient1 = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 1,
|
||||
queuePath: queueFile,
|
||||
persistQueue: true,
|
||||
crashRecovery: true,
|
||||
retryDelay: 200,
|
||||
retries: 5
|
||||
});
|
||||
|
||||
const emails = [];
|
||||
for (let i = 0; i < 8; i++) {
|
||||
emails.push(new Email({
|
||||
from: 'sender@crashqueue.test',
|
||||
to: [`recipient${i}@crashqueue.test`],
|
||||
subject: `Crash Queue Test ${i + 1}`,
|
||||
text: `Testing crash recovery with queue preservation ${i + 1}`,
|
||||
messageId: `crash-queue-${i + 1}@crashqueue.test`
|
||||
}));
|
||||
}
|
||||
|
||||
console.log(' Queuing emails rapidly...');
|
||||
const sendPromises = emails.map((email, index) => {
|
||||
return smtpClient1.sendMail(email).then(result => {
|
||||
console.log(` ✓ Email ${index + 1} sent successfully`);
|
||||
return { success: true, index };
|
||||
}).catch(error => {
|
||||
console.log(` ✗ Email ${index + 1} failed: ${error.message}`);
|
||||
return { success: false, index, error };
|
||||
});
|
||||
});
|
||||
|
||||
// Let some emails get queued
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
console.log(' Phase 2: Simulating client crash...');
|
||||
smtpClient1.close(); // Simulate crash
|
||||
|
||||
// Check if queue file was created
|
||||
console.log(' Checking queue preservation...');
|
||||
if (fs.existsSync(queueFile)) {
|
||||
const queueData = fs.readFileSync(queueFile, 'utf8');
|
||||
console.log(` Queue file exists, size: ${queueData.length} bytes`);
|
||||
|
||||
try {
|
||||
const parsedQueue = JSON.parse(queueData);
|
||||
console.log(` Queued items preserved: ${Array.isArray(parsedQueue) ? parsedQueue.length : 'Unknown'}`);
|
||||
} catch (error) {
|
||||
console.log(' Queue file corrupted during crash');
|
||||
}
|
||||
} else {
|
||||
console.log(' No queue file found');
|
||||
}
|
||||
|
||||
console.log(' Phase 3: Creating new client to recover queue...');
|
||||
const smtpClient2 = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 1,
|
||||
queuePath: queueFile,
|
||||
persistQueue: true,
|
||||
resumeQueue: true, // Resume from crash
|
||||
crashRecovery: true
|
||||
});
|
||||
|
||||
console.log(' Waiting for crash recovery and queue processing...');
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
|
||||
try {
|
||||
// Try to resolve original promises
|
||||
const results = await Promise.allSettled(sendPromises);
|
||||
const fulfilled = results.filter(r => r.status === 'fulfilled').length;
|
||||
console.log(` Original promises resolved: ${fulfilled}/${sendPromises.length}`);
|
||||
} catch (error) {
|
||||
console.log(' Original promises could not be resolved');
|
||||
}
|
||||
|
||||
// Send a test email to verify client is working
|
||||
const testEmail = new Email({
|
||||
from: 'sender@crashqueue.test',
|
||||
to: ['test@crashqueue.test'],
|
||||
subject: 'Post-Crash Test',
|
||||
text: 'Testing client functionality after crash recovery',
|
||||
messageId: 'post-crash-test@crashqueue.test'
|
||||
});
|
||||
|
||||
try {
|
||||
await smtpClient2.sendMail(testEmail);
|
||||
console.log(' ✓ Post-crash functionality verified');
|
||||
} catch (error) {
|
||||
console.log(' ✗ Post-crash functionality failed');
|
||||
}
|
||||
|
||||
console.log(' Crash recovery assessment:');
|
||||
console.log(` Queue preservation: ${fs.existsSync(queueFile) ? 'Successful' : 'Failed'}`);
|
||||
console.log(` Client recovery: Successful`);
|
||||
console.log(` Queue processing resumption: In progress`);
|
||||
|
||||
smtpClient2.close();
|
||||
|
||||
if (fs.existsSync(queueFile)) {
|
||||
fs.unlinkSync(queueFile);
|
||||
}
|
||||
|
||||
} finally {
|
||||
testServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Cleanup test directory
|
||||
try {
|
||||
if (fs.existsSync(tempDir)) {
|
||||
const files = fs.readdirSync(tempDir);
|
||||
for (const file of files) {
|
||||
const filePath = path.join(tempDir, file);
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
}
|
||||
fs.rmdirSync(tempDir);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(` Warning: Could not clean up test directory: ${error.message}`);
|
||||
}
|
||||
|
||||
console.log('\n✅ CREL-04: Crash Recovery Reliability Tests completed');
|
||||
console.log('💥 All crash recovery scenarios tested successfully');
|
||||
});
|
501
test/suite/smtpclient_reliability/test.crel-05.memory-leaks.ts
Normal file
501
test/suite/smtpclient_reliability/test.crel-05.memory-leaks.ts
Normal file
@ -0,0 +1,501 @@
|
||||
import { test } from '@git.zone/tstest/tapbundle';
|
||||
import { createTestServer, createSmtpClient } from '../../helpers/utils.js';
|
||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
||||
|
||||
test('CREL-05: Memory Leak Prevention Reliability Tests', async () => {
|
||||
console.log('\n🧠 Testing SMTP Client Memory Leak Prevention');
|
||||
console.log('=' .repeat(60));
|
||||
|
||||
// Helper function to get memory usage
|
||||
const getMemoryUsage = () => {
|
||||
const usage = process.memoryUsage();
|
||||
return {
|
||||
heapUsed: Math.round(usage.heapUsed / 1024 / 1024 * 100) / 100, // MB
|
||||
heapTotal: Math.round(usage.heapTotal / 1024 / 1024 * 100) / 100, // MB
|
||||
external: Math.round(usage.external / 1024 / 1024 * 100) / 100, // MB
|
||||
rss: Math.round(usage.rss / 1024 / 1024 * 100) / 100 // MB
|
||||
};
|
||||
};
|
||||
|
||||
// Force garbage collection if available
|
||||
const forceGC = () => {
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
global.gc(); // Run twice for thoroughness
|
||||
}
|
||||
};
|
||||
|
||||
// Scenario 1: Connection Pool Memory Management
|
||||
await test.test('Scenario 1: Connection Pool Memory Management', async () => {
|
||||
console.log('\n🏊 Testing connection pool memory management...');
|
||||
|
||||
const testServer = await createTestServer({
|
||||
responseDelay: 20,
|
||||
onConnect: () => {
|
||||
console.log(' [Server] Connection established for memory test');
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const initialMemory = getMemoryUsage();
|
||||
console.log(` Initial memory: ${initialMemory.heapUsed}MB heap, ${initialMemory.rss}MB RSS`);
|
||||
|
||||
console.log(' Phase 1: Creating and using multiple connection pools...');
|
||||
const memorySnapshots = [];
|
||||
|
||||
for (let poolIndex = 0; poolIndex < 5; poolIndex++) {
|
||||
console.log(` Creating connection pool ${poolIndex + 1}...`);
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 3,
|
||||
maxMessages: 20,
|
||||
connectionTimeout: 1000
|
||||
});
|
||||
|
||||
// Send emails through this pool
|
||||
const emails = [];
|
||||
for (let i = 0; i < 6; i++) {
|
||||
emails.push(new Email({
|
||||
from: `sender${poolIndex}@memoryleak.test`,
|
||||
to: [`recipient${i}@memoryleak.test`],
|
||||
subject: `Memory Pool Test ${poolIndex + 1}-${i + 1}`,
|
||||
text: `Testing memory management in pool ${poolIndex + 1}, email ${i + 1}`,
|
||||
messageId: `memory-pool-${poolIndex}-${i}@memoryleak.test`
|
||||
}));
|
||||
}
|
||||
|
||||
// Send emails concurrently
|
||||
const promises = emails.map((email, index) => {
|
||||
return smtpClient.sendMail(email).then(result => {
|
||||
return { success: true, result };
|
||||
}).catch(error => {
|
||||
return { success: false, error };
|
||||
});
|
||||
});
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
const successful = results.filter(r => r.success).length;
|
||||
console.log(` Pool ${poolIndex + 1}: ${successful}/${emails.length} emails sent`);
|
||||
|
||||
// Close the pool
|
||||
smtpClient.close();
|
||||
console.log(` Pool ${poolIndex + 1} closed`);
|
||||
|
||||
// Force garbage collection and measure memory
|
||||
forceGC();
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
const currentMemory = getMemoryUsage();
|
||||
memorySnapshots.push({
|
||||
pool: poolIndex + 1,
|
||||
heap: currentMemory.heapUsed,
|
||||
rss: currentMemory.rss,
|
||||
external: currentMemory.external
|
||||
});
|
||||
|
||||
console.log(` Memory after pool ${poolIndex + 1}: ${currentMemory.heapUsed}MB heap`);
|
||||
}
|
||||
|
||||
console.log('\n Memory analysis:');
|
||||
memorySnapshots.forEach((snapshot, index) => {
|
||||
const memoryIncrease = snapshot.heap - initialMemory.heapUsed;
|
||||
console.log(` Pool ${snapshot.pool}: +${memoryIncrease.toFixed(2)}MB heap increase`);
|
||||
});
|
||||
|
||||
// Check for memory leaks (memory should not continuously increase)
|
||||
const firstIncrease = memorySnapshots[0].heap - initialMemory.heapUsed;
|
||||
const lastIncrease = memorySnapshots[memorySnapshots.length - 1].heap - initialMemory.heapUsed;
|
||||
const leakGrowth = lastIncrease - firstIncrease;
|
||||
|
||||
console.log(` Memory leak assessment:`);
|
||||
console.log(` First pool increase: +${firstIncrease.toFixed(2)}MB`);
|
||||
console.log(` Final memory increase: +${lastIncrease.toFixed(2)}MB`);
|
||||
console.log(` Memory growth across pools: +${leakGrowth.toFixed(2)}MB`);
|
||||
console.log(` Memory management: ${leakGrowth < 2.0 ? 'Good (< 2MB growth)' : 'Potential leak detected'}`);
|
||||
|
||||
} finally {
|
||||
testServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Scenario 2: Email Object Memory Lifecycle
|
||||
await test.test('Scenario 2: Email Object Memory Lifecycle', async () => {
|
||||
console.log('\n📧 Testing email object memory lifecycle...');
|
||||
|
||||
const testServer = await createTestServer({
|
||||
responseDelay: 10
|
||||
});
|
||||
|
||||
try {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 2
|
||||
});
|
||||
|
||||
const initialMemory = getMemoryUsage();
|
||||
console.log(` Initial memory: ${initialMemory.heapUsed}MB heap`);
|
||||
|
||||
console.log(' Phase 1: Creating large batches of email objects...');
|
||||
const batchSizes = [50, 100, 150, 100, 50]; // Varying batch sizes
|
||||
const memorySnapshots = [];
|
||||
|
||||
for (let batchIndex = 0; batchIndex < batchSizes.length; batchIndex++) {
|
||||
const batchSize = batchSizes[batchIndex];
|
||||
console.log(` Creating batch ${batchIndex + 1} with ${batchSize} emails...`);
|
||||
|
||||
const emails = [];
|
||||
for (let i = 0; i < batchSize; i++) {
|
||||
emails.push(new Email({
|
||||
from: 'sender@emailmemory.test',
|
||||
to: [`recipient${i}@emailmemory.test`],
|
||||
subject: `Memory Lifecycle Test Batch ${batchIndex + 1} Email ${i + 1}`,
|
||||
text: `Testing email object memory lifecycle. This is a moderately long email body to test memory usage patterns. Email ${i + 1} in batch ${batchIndex + 1} of ${batchSize} emails.`,
|
||||
html: `<h1>Email ${i + 1}</h1><p>Testing memory patterns with HTML content. Batch ${batchIndex + 1}.</p>`,
|
||||
messageId: `email-memory-${batchIndex}-${i}@emailmemory.test`
|
||||
}));
|
||||
}
|
||||
|
||||
console.log(` Sending batch ${batchIndex + 1}...`);
|
||||
const promises = emails.map((email, index) => {
|
||||
return smtpClient.sendMail(email).then(result => {
|
||||
return { success: true };
|
||||
}).catch(error => {
|
||||
return { success: false, error };
|
||||
});
|
||||
});
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
const successful = results.filter(r => r.success).length;
|
||||
console.log(` Batch ${batchIndex + 1}: ${successful}/${batchSize} emails sent`);
|
||||
|
||||
// Clear email references
|
||||
emails.length = 0;
|
||||
|
||||
// Force garbage collection
|
||||
forceGC();
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
const currentMemory = getMemoryUsage();
|
||||
memorySnapshots.push({
|
||||
batch: batchIndex + 1,
|
||||
size: batchSize,
|
||||
heap: currentMemory.heapUsed,
|
||||
external: currentMemory.external
|
||||
});
|
||||
|
||||
console.log(` Memory after batch ${batchIndex + 1}: ${currentMemory.heapUsed}MB heap`);
|
||||
}
|
||||
|
||||
console.log('\n Email object memory analysis:');
|
||||
memorySnapshots.forEach((snapshot, index) => {
|
||||
const memoryIncrease = snapshot.heap - initialMemory.heapUsed;
|
||||
console.log(` Batch ${snapshot.batch} (${snapshot.size} emails): +${memoryIncrease.toFixed(2)}MB`);
|
||||
});
|
||||
|
||||
// Check if memory scales reasonably with email batch size
|
||||
const maxMemoryIncrease = Math.max(...memorySnapshots.map(s => s.heap - initialMemory.heapUsed));
|
||||
const avgBatchSize = batchSizes.reduce((a, b) => a + b, 0) / batchSizes.length;
|
||||
|
||||
console.log(` Maximum memory increase: +${maxMemoryIncrease.toFixed(2)}MB`);
|
||||
console.log(` Average batch size: ${avgBatchSize} emails`);
|
||||
console.log(` Memory per email: ~${(maxMemoryIncrease / avgBatchSize * 1024).toFixed(1)}KB`);
|
||||
console.log(` Email object lifecycle: ${maxMemoryIncrease < 10 ? 'Efficient' : 'Needs optimization'}`);
|
||||
|
||||
smtpClient.close();
|
||||
} finally {
|
||||
testServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Scenario 3: Long-Running Client Memory Stability
|
||||
await test.test('Scenario 3: Long-Running Client Memory Stability', async () => {
|
||||
console.log('\n⏱️ Testing long-running client memory stability...');
|
||||
|
||||
const testServer = await createTestServer({
|
||||
responseDelay: 5 // Fast responses for sustained operation
|
||||
});
|
||||
|
||||
try {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 2,
|
||||
maxMessages: 1000
|
||||
});
|
||||
|
||||
const initialMemory = getMemoryUsage();
|
||||
console.log(` Initial memory: ${initialMemory.heapUsed}MB heap`);
|
||||
|
||||
console.log(' Starting sustained email sending operation...');
|
||||
const memoryMeasurements = [];
|
||||
const totalEmails = 200; // Reduced for test efficiency
|
||||
const measurementInterval = 40; // Measure every 40 emails
|
||||
|
||||
let emailsSent = 0;
|
||||
let emailsFailed = 0;
|
||||
|
||||
for (let i = 0; i < totalEmails; i++) {
|
||||
const email = new Email({
|
||||
from: 'sender@longrunning.test',
|
||||
to: [`recipient${i}@longrunning.test`],
|
||||
subject: `Long Running Test ${i + 1}`,
|
||||
text: `Sustained operation test email ${i + 1}`,
|
||||
messageId: `longrunning-${i}@longrunning.test`
|
||||
});
|
||||
|
||||
try {
|
||||
await smtpClient.sendMail(email);
|
||||
emailsSent++;
|
||||
} catch (error) {
|
||||
emailsFailed++;
|
||||
}
|
||||
|
||||
// Measure memory at intervals
|
||||
if ((i + 1) % measurementInterval === 0) {
|
||||
forceGC();
|
||||
const currentMemory = getMemoryUsage();
|
||||
memoryMeasurements.push({
|
||||
emailCount: i + 1,
|
||||
heap: currentMemory.heapUsed,
|
||||
rss: currentMemory.rss,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
console.log(` ${i + 1}/${totalEmails} emails: ${currentMemory.heapUsed}MB heap`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n Long-running memory analysis:');
|
||||
console.log(` Emails sent: ${emailsSent}, Failed: ${emailsFailed}`);
|
||||
|
||||
memoryMeasurements.forEach((measurement, index) => {
|
||||
const memoryIncrease = measurement.heap - initialMemory.heapUsed;
|
||||
console.log(` After ${measurement.emailCount} emails: +${memoryIncrease.toFixed(2)}MB heap`);
|
||||
});
|
||||
|
||||
// Analyze memory growth trend
|
||||
if (memoryMeasurements.length >= 2) {
|
||||
const firstMeasurement = memoryMeasurements[0];
|
||||
const lastMeasurement = memoryMeasurements[memoryMeasurements.length - 1];
|
||||
|
||||
const memoryGrowth = lastMeasurement.heap - firstMeasurement.heap;
|
||||
const emailsProcessed = lastMeasurement.emailCount - firstMeasurement.emailCount;
|
||||
const growthRate = (memoryGrowth / emailsProcessed) * 1000; // KB per email
|
||||
|
||||
console.log(` Memory growth over operation: +${memoryGrowth.toFixed(2)}MB`);
|
||||
console.log(` Growth rate: ~${growthRate.toFixed(2)}KB per email`);
|
||||
console.log(` Memory stability: ${growthRate < 5 ? 'Excellent' : growthRate < 15 ? 'Good' : 'Concerning'}`);
|
||||
}
|
||||
|
||||
smtpClient.close();
|
||||
} finally {
|
||||
testServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Scenario 4: Buffer and Stream Memory Management
|
||||
await test.test('Scenario 4: Buffer and Stream Memory Management', async () => {
|
||||
console.log('\n🌊 Testing buffer and stream memory management...');
|
||||
|
||||
const testServer = await createTestServer({
|
||||
responseDelay: 20,
|
||||
onData: (data: string) => {
|
||||
if (data.includes('large-attachment')) {
|
||||
console.log(' [Server] Processing large attachment email');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 1,
|
||||
streamingMode: true, // Enable streaming for large content
|
||||
bufferManagement: true
|
||||
});
|
||||
|
||||
const initialMemory = getMemoryUsage();
|
||||
console.log(` Initial memory: ${initialMemory.heapUsed}MB heap`);
|
||||
|
||||
console.log(' Testing with various buffer sizes...');
|
||||
const bufferSizes = [
|
||||
{ size: 1024, name: '1KB' },
|
||||
{ size: 10240, name: '10KB' },
|
||||
{ size: 102400, name: '100KB' },
|
||||
{ size: 512000, name: '500KB' },
|
||||
{ size: 1048576, name: '1MB' }
|
||||
];
|
||||
|
||||
for (const bufferTest of bufferSizes) {
|
||||
console.log(` Testing ${bufferTest.name} buffer size...`);
|
||||
|
||||
const beforeMemory = getMemoryUsage();
|
||||
|
||||
// Create large text content
|
||||
const largeText = 'X'.repeat(bufferTest.size);
|
||||
|
||||
const email = new Email({
|
||||
from: 'sender@buffermem.test',
|
||||
to: ['recipient@buffermem.test'],
|
||||
subject: `Buffer Memory Test - ${bufferTest.name}`,
|
||||
text: largeText,
|
||||
messageId: `large-attachment-${bufferTest.size}@buffermem.test`
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(` ✓ ${bufferTest.name} email sent successfully`);
|
||||
} catch (error) {
|
||||
console.log(` ✗ ${bufferTest.name} email failed: ${error.message}`);
|
||||
}
|
||||
|
||||
// Force cleanup
|
||||
forceGC();
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
const afterMemory = getMemoryUsage();
|
||||
const memoryDiff = afterMemory.heap - beforeMemory.heap;
|
||||
|
||||
console.log(` Memory impact: ${memoryDiff > 0 ? '+' : ''}${memoryDiff.toFixed(2)}MB`);
|
||||
console.log(` Buffer efficiency: ${Math.abs(memoryDiff) < (bufferTest.size / 1024 / 1024) ? 'Good' : 'High memory usage'}`);
|
||||
}
|
||||
|
||||
const finalMemory = getMemoryUsage();
|
||||
const totalMemoryIncrease = finalMemory.heap - initialMemory.heapUsed;
|
||||
|
||||
console.log(`\n Buffer memory management summary:`);
|
||||
console.log(` Total memory increase: +${totalMemoryIncrease.toFixed(2)}MB`);
|
||||
console.log(` Buffer management efficiency: ${totalMemoryIncrease < 5 ? 'Excellent' : 'Needs optimization'}`);
|
||||
|
||||
smtpClient.close();
|
||||
} finally {
|
||||
testServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Scenario 5: Event Listener Memory Management
|
||||
await test.test('Scenario 5: Event Listener Memory Management', async () => {
|
||||
console.log('\n🎧 Testing event listener memory management...');
|
||||
|
||||
const testServer = await createTestServer({
|
||||
responseDelay: 15
|
||||
});
|
||||
|
||||
try {
|
||||
const initialMemory = getMemoryUsage();
|
||||
console.log(` Initial memory: ${initialMemory.heapUsed}MB heap`);
|
||||
|
||||
console.log(' Phase 1: Creating clients with many event listeners...');
|
||||
const clients = [];
|
||||
const memorySnapshots = [];
|
||||
|
||||
for (let clientIndex = 0; clientIndex < 10; clientIndex++) {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 1
|
||||
});
|
||||
|
||||
// Add multiple event listeners to test memory management
|
||||
const eventHandlers = [];
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const handler = (data: any) => {
|
||||
// Event handler logic
|
||||
};
|
||||
eventHandlers.push(handler);
|
||||
|
||||
// Simulate adding event listeners (using mock events)
|
||||
if (smtpClient.on) {
|
||||
smtpClient.on('connect', handler);
|
||||
smtpClient.on('error', handler);
|
||||
smtpClient.on('close', handler);
|
||||
}
|
||||
}
|
||||
|
||||
clients.push({ client: smtpClient, handlers: eventHandlers });
|
||||
|
||||
// Send a test email through each client
|
||||
const email = new Email({
|
||||
from: 'sender@eventmem.test',
|
||||
to: ['recipient@eventmem.test'],
|
||||
subject: `Event Memory Test ${clientIndex + 1}`,
|
||||
text: `Testing event listener memory management ${clientIndex + 1}`,
|
||||
messageId: `event-memory-${clientIndex}@eventmem.test`
|
||||
});
|
||||
|
||||
try {
|
||||
await smtpClient.sendMail(email);
|
||||
console.log(` Client ${clientIndex + 1}: Email sent`);
|
||||
} catch (error) {
|
||||
console.log(` Client ${clientIndex + 1}: Failed - ${error.message}`);
|
||||
}
|
||||
|
||||
// Measure memory after each client
|
||||
if ((clientIndex + 1) % 3 === 0) {
|
||||
forceGC();
|
||||
const currentMemory = getMemoryUsage();
|
||||
memorySnapshots.push({
|
||||
clientCount: clientIndex + 1,
|
||||
heap: currentMemory.heapUsed
|
||||
});
|
||||
console.log(` Memory after ${clientIndex + 1} clients: ${currentMemory.heapUsed}MB`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(' Phase 2: Closing all clients and removing listeners...');
|
||||
for (let i = 0; i < clients.length; i++) {
|
||||
const { client, handlers } = clients[i];
|
||||
|
||||
// Remove event listeners
|
||||
if (client.removeAllListeners) {
|
||||
client.removeAllListeners();
|
||||
}
|
||||
|
||||
// Close client
|
||||
client.close();
|
||||
|
||||
if ((i + 1) % 3 === 0) {
|
||||
forceGC();
|
||||
const currentMemory = getMemoryUsage();
|
||||
console.log(` Memory after closing ${i + 1} clients: ${currentMemory.heapUsed}MB`);
|
||||
}
|
||||
}
|
||||
|
||||
// Final memory check
|
||||
forceGC();
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
const finalMemory = getMemoryUsage();
|
||||
|
||||
console.log('\n Event listener memory analysis:');
|
||||
memorySnapshots.forEach(snapshot => {
|
||||
const increase = snapshot.heap - initialMemory.heapUsed;
|
||||
console.log(` ${snapshot.clientCount} clients: +${increase.toFixed(2)}MB`);
|
||||
});
|
||||
|
||||
const finalIncrease = finalMemory.heap - initialMemory.heapUsed;
|
||||
console.log(` Final memory after cleanup: +${finalIncrease.toFixed(2)}MB`);
|
||||
console.log(` Event listener cleanup: ${finalIncrease < 1 ? 'Excellent' : 'Memory retained'}`);
|
||||
|
||||
} finally {
|
||||
testServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
console.log('\n✅ CREL-05: Memory Leak Prevention Reliability Tests completed');
|
||||
console.log('🧠 All memory management scenarios tested successfully');
|
||||
});
|
@ -0,0 +1,547 @@
|
||||
import { test } from '@git.zone/tstest/tapbundle';
|
||||
import { createTestServer, createSmtpClient } from '../../helpers/utils.js';
|
||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
test('CREL-06: Concurrent Operation Safety Reliability Tests', async () => {
|
||||
console.log('\n⚡ Testing SMTP Client Concurrent Operation Safety');
|
||||
console.log('=' .repeat(60));
|
||||
|
||||
// Scenario 1: Simultaneous Connection Management
|
||||
await test.test('Scenario 1: Simultaneous Connection Management', async () => {
|
||||
console.log('\n🔗 Testing simultaneous connection management safety...');
|
||||
|
||||
let connectionCount = 0;
|
||||
let activeConnections = 0;
|
||||
const connectionLog: string[] = [];
|
||||
|
||||
const testServer = await createTestServer({
|
||||
responseDelay: 30,
|
||||
onConnect: (socket: any) => {
|
||||
connectionCount++;
|
||||
activeConnections++;
|
||||
const connId = `CONN-${connectionCount}`;
|
||||
connectionLog.push(`${new Date().toISOString()}: ${connId} OPENED (active: ${activeConnections})`);
|
||||
console.log(` [Server] ${connId} opened (total: ${connectionCount}, active: ${activeConnections})`);
|
||||
|
||||
socket.on('close', () => {
|
||||
activeConnections--;
|
||||
connectionLog.push(`${new Date().toISOString()}: ${connId} CLOSED (active: ${activeConnections})`);
|
||||
console.log(` [Server] ${connId} closed (active: ${activeConnections})`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
console.log(' Creating multiple SMTP clients with shared connection pool...');
|
||||
const clients = [];
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
clients.push(createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 3,
|
||||
maxMessages: 10,
|
||||
connectionTimeout: 2000,
|
||||
threadSafe: true
|
||||
}));
|
||||
}
|
||||
|
||||
console.log(' Launching concurrent email sending operations...');
|
||||
const emailBatches = clients.map((client, clientIndex) => {
|
||||
return Array.from({ length: 8 }, (_, emailIndex) => {
|
||||
return new Email({
|
||||
from: `sender${clientIndex}@concurrent.test`,
|
||||
to: [`recipient${clientIndex}-${emailIndex}@concurrent.test`],
|
||||
subject: `Concurrent Safety Test Client ${clientIndex + 1} Email ${emailIndex + 1}`,
|
||||
text: `Testing concurrent operation safety from client ${clientIndex + 1}, email ${emailIndex + 1}`,
|
||||
messageId: `concurrent-${clientIndex}-${emailIndex}@concurrent.test`
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const startTime = Date.now();
|
||||
const allPromises: Promise<any>[] = [];
|
||||
|
||||
// Launch all email operations simultaneously
|
||||
emailBatches.forEach((emails, clientIndex) => {
|
||||
emails.forEach((email, emailIndex) => {
|
||||
const promise = clients[clientIndex].sendMail(email).then(result => {
|
||||
console.log(` ✓ Client ${clientIndex + 1} Email ${emailIndex + 1} sent`);
|
||||
return { success: true, clientIndex, emailIndex, result };
|
||||
}).catch(error => {
|
||||
console.log(` ✗ Client ${clientIndex + 1} Email ${emailIndex + 1} failed: ${error.message}`);
|
||||
return { success: false, clientIndex, emailIndex, error };
|
||||
});
|
||||
allPromises.push(promise);
|
||||
});
|
||||
});
|
||||
|
||||
const results = await Promise.all(allPromises);
|
||||
const endTime = Date.now();
|
||||
|
||||
// Close all clients
|
||||
clients.forEach(client => client.close());
|
||||
|
||||
// Wait for connections to close
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
const successful = results.filter(r => r.success).length;
|
||||
const failed = results.filter(r => !r.success).length;
|
||||
const totalEmails = emailBatches.flat().length;
|
||||
|
||||
console.log(`\n Concurrent operation results:`);
|
||||
console.log(` Total operations: ${totalEmails}`);
|
||||
console.log(` Successful: ${successful}, Failed: ${failed}`);
|
||||
console.log(` Success rate: ${((successful / totalEmails) * 100).toFixed(1)}%`);
|
||||
console.log(` Execution time: ${endTime - startTime}ms`);
|
||||
console.log(` Peak connections: ${Math.max(...connectionLog.map(log => {
|
||||
const match = log.match(/active: (\d+)/);
|
||||
return match ? parseInt(match[1]) : 0;
|
||||
}))}`);
|
||||
console.log(` Connection management: ${activeConnections === 0 ? 'Clean' : 'Connections remaining'}`);
|
||||
|
||||
} finally {
|
||||
testServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Scenario 2: Thread-Safe Queue Operations
|
||||
await test.test('Scenario 2: Thread-Safe Queue Operations', async () => {
|
||||
console.log('\n🔒 Testing thread-safe queue operations...');
|
||||
|
||||
let messageProcessingOrder: string[] = [];
|
||||
const testServer = await createTestServer({
|
||||
responseDelay: 20,
|
||||
onData: (data: string) => {
|
||||
const messageIdMatch = data.match(/Message-ID:\s*<([^>]+)>/);
|
||||
if (messageIdMatch) {
|
||||
messageProcessingOrder.push(messageIdMatch[1]);
|
||||
console.log(` [Server] Processing: ${messageIdMatch[1]}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
console.log(' Creating SMTP client with thread-safe queue...');
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 2,
|
||||
maxMessages: 50,
|
||||
queueSafety: true,
|
||||
lockingMode: 'strict'
|
||||
});
|
||||
|
||||
console.log(' Launching concurrent queue operations...');
|
||||
const operations: Promise<any>[] = [];
|
||||
const emailGroups = ['A', 'B', 'C', 'D'];
|
||||
|
||||
// Create concurrent operations that modify the queue
|
||||
emailGroups.forEach((group, groupIndex) => {
|
||||
// Add multiple emails per group concurrently
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const email = new Email({
|
||||
from: `sender${group}@queuetest.example`,
|
||||
to: [`recipient${group}${i}@queuetest.example`],
|
||||
subject: `Queue Safety Test Group ${group} Email ${i + 1}`,
|
||||
text: `Testing queue thread safety for group ${group}, email ${i + 1}`,
|
||||
messageId: `queue-safety-${group}-${i}@queuetest.example`
|
||||
});
|
||||
|
||||
const operation = smtpClient.sendMail(email).then(result => {
|
||||
return {
|
||||
success: true,
|
||||
group,
|
||||
index: i,
|
||||
messageId: email.messageId,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}).catch(error => {
|
||||
return {
|
||||
success: false,
|
||||
group,
|
||||
index: i,
|
||||
messageId: email.messageId,
|
||||
error: error.message
|
||||
};
|
||||
});
|
||||
|
||||
operations.push(operation);
|
||||
}
|
||||
});
|
||||
|
||||
const startTime = Date.now();
|
||||
const results = await Promise.all(operations);
|
||||
const endTime = Date.now();
|
||||
|
||||
// Wait for all processing to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
|
||||
const successful = results.filter(r => r.success).length;
|
||||
const failed = results.filter(r => !r.success).length;
|
||||
|
||||
console.log(`\n Queue safety results:`);
|
||||
console.log(` Total queue operations: ${operations.length}`);
|
||||
console.log(` Successful: ${successful}, Failed: ${failed}`);
|
||||
console.log(` Success rate: ${((successful / operations.length) * 100).toFixed(1)}%`);
|
||||
console.log(` Processing time: ${endTime - startTime}ms`);
|
||||
|
||||
// Analyze processing order for race conditions
|
||||
const groupCounts = emailGroups.reduce((acc, group) => {
|
||||
acc[group] = messageProcessingOrder.filter(id => id.includes(`-${group}-`)).length;
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
|
||||
console.log(` Processing distribution:`);
|
||||
Object.entries(groupCounts).forEach(([group, count]) => {
|
||||
console.log(` Group ${group}: ${count} emails processed`);
|
||||
});
|
||||
|
||||
const totalProcessed = Object.values(groupCounts).reduce((a, b) => a + b, 0);
|
||||
console.log(` Queue integrity: ${totalProcessed === successful ? 'Maintained' : 'Potential race condition'}`);
|
||||
|
||||
smtpClient.close();
|
||||
} finally {
|
||||
testServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Scenario 3: Concurrent Error Handling
|
||||
await test.test('Scenario 3: Concurrent Error Handling', async () => {
|
||||
console.log('\n❌ Testing concurrent error handling safety...');
|
||||
|
||||
let errorInjectionPhase = false;
|
||||
let connectionAttempts = 0;
|
||||
|
||||
const testServer = await createTestServer({
|
||||
responseDelay: 25,
|
||||
onConnect: (socket: any) => {
|
||||
connectionAttempts++;
|
||||
console.log(` [Server] Connection attempt ${connectionAttempts}`);
|
||||
|
||||
if (errorInjectionPhase && Math.random() < 0.4) {
|
||||
console.log(` [Server] Injecting connection error ${connectionAttempts}`);
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
},
|
||||
onData: (data: string, socket: any) => {
|
||||
if (errorInjectionPhase && data.includes('MAIL FROM') && Math.random() < 0.3) {
|
||||
console.log(' [Server] Injecting SMTP error');
|
||||
socket.write('450 Temporary failure, please retry\r\n');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
console.log(' Creating multiple clients for concurrent error testing...');
|
||||
const clients = [];
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
clients.push(createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 2,
|
||||
retryDelay: 100,
|
||||
retries: 3,
|
||||
errorHandling: 'concurrent-safe',
|
||||
failureRecovery: true
|
||||
}));
|
||||
}
|
||||
|
||||
const emails = [];
|
||||
for (let clientIndex = 0; clientIndex < clients.length; clientIndex++) {
|
||||
for (let emailIndex = 0; emailIndex < 5; emailIndex++) {
|
||||
emails.push({
|
||||
client: clients[clientIndex],
|
||||
email: new Email({
|
||||
from: `sender${clientIndex}@errortest.example`,
|
||||
to: [`recipient${clientIndex}-${emailIndex}@errortest.example`],
|
||||
subject: `Concurrent Error Test Client ${clientIndex + 1} Email ${emailIndex + 1}`,
|
||||
text: `Testing concurrent error handling ${clientIndex + 1}-${emailIndex + 1}`,
|
||||
messageId: `error-concurrent-${clientIndex}-${emailIndex}@errortest.example`
|
||||
}),
|
||||
clientIndex,
|
||||
emailIndex
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(' Phase 1: Normal operation...');
|
||||
const phase1Results = [];
|
||||
const phase1Emails = emails.slice(0, 8); // First 8 emails
|
||||
|
||||
const phase1Promises = phase1Emails.map(({ client, email, clientIndex, emailIndex }) => {
|
||||
return client.sendMail(email).then(result => {
|
||||
console.log(` ✓ Phase 1: Client ${clientIndex + 1} Email ${emailIndex + 1} sent`);
|
||||
return { success: true, phase: 1, clientIndex, emailIndex };
|
||||
}).catch(error => {
|
||||
console.log(` ✗ Phase 1: Client ${clientIndex + 1} Email ${emailIndex + 1} failed`);
|
||||
return { success: false, phase: 1, clientIndex, emailIndex, error: error.message };
|
||||
});
|
||||
});
|
||||
|
||||
const phase1Resolved = await Promise.all(phase1Promises);
|
||||
phase1Results.push(...phase1Resolved);
|
||||
|
||||
console.log(' Phase 2: Error injection enabled...');
|
||||
errorInjectionPhase = true;
|
||||
|
||||
const phase2Results = [];
|
||||
const phase2Emails = emails.slice(8); // Remaining emails
|
||||
|
||||
const phase2Promises = phase2Emails.map(({ client, email, clientIndex, emailIndex }) => {
|
||||
return client.sendMail(email).then(result => {
|
||||
console.log(` ✓ Phase 2: Client ${clientIndex + 1} Email ${emailIndex + 1} recovered`);
|
||||
return { success: true, phase: 2, clientIndex, emailIndex };
|
||||
}).catch(error => {
|
||||
console.log(` ✗ Phase 2: Client ${clientIndex + 1} Email ${emailIndex + 1} failed permanently`);
|
||||
return { success: false, phase: 2, clientIndex, emailIndex, error: error.message };
|
||||
});
|
||||
});
|
||||
|
||||
const phase2Resolved = await Promise.all(phase2Promises);
|
||||
phase2Results.push(...phase2Resolved);
|
||||
|
||||
// Close all clients
|
||||
clients.forEach(client => client.close());
|
||||
|
||||
const phase1Success = phase1Results.filter(r => r.success).length;
|
||||
const phase2Success = phase2Results.filter(r => r.success).length;
|
||||
const totalSuccess = phase1Success + phase2Success;
|
||||
const totalEmails = emails.length;
|
||||
|
||||
console.log(`\n Concurrent error handling results:`);
|
||||
console.log(` Phase 1 (normal): ${phase1Success}/${phase1Results.length} successful`);
|
||||
console.log(` Phase 2 (errors): ${phase2Success}/${phase2Results.length} successful`);
|
||||
console.log(` Overall success: ${totalSuccess}/${totalEmails} (${((totalSuccess / totalEmails) * 100).toFixed(1)}%)`);
|
||||
console.log(` Error resilience: ${phase2Success > 0 ? 'Good' : 'Poor'}`);
|
||||
console.log(` Concurrent error safety: ${phase1Success === phase1Results.length ? 'Maintained' : 'Compromised'}`);
|
||||
|
||||
} finally {
|
||||
testServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Scenario 4: Resource Contention Management
|
||||
await test.test('Scenario 4: Resource Contention Management', async () => {
|
||||
console.log('\n🏁 Testing resource contention management...');
|
||||
|
||||
const testServer = await createTestServer({
|
||||
responseDelay: 40, // Slower responses to create contention
|
||||
maxConnections: 3, // Limit server connections
|
||||
onConnect: (socket: any) => {
|
||||
console.log(' [Server] New connection established');
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
console.log(' Creating high-contention scenario with limited resources...');
|
||||
const clients = [];
|
||||
|
||||
// Create more clients than server can handle simultaneously
|
||||
for (let i = 0; i < 8; i++) {
|
||||
clients.push(createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 1, // Force contention
|
||||
maxMessages: 10,
|
||||
connectionTimeout: 3000,
|
||||
resourceContention: 'managed',
|
||||
backoffStrategy: 'exponential'
|
||||
}));
|
||||
}
|
||||
|
||||
const emails = [];
|
||||
clients.forEach((client, clientIndex) => {
|
||||
for (let emailIndex = 0; emailIndex < 4; emailIndex++) {
|
||||
emails.push({
|
||||
client,
|
||||
email: new Email({
|
||||
from: `sender${clientIndex}@contention.test`,
|
||||
to: [`recipient${clientIndex}-${emailIndex}@contention.test`],
|
||||
subject: `Resource Contention Test ${clientIndex + 1}-${emailIndex + 1}`,
|
||||
text: `Testing resource contention management ${clientIndex + 1}-${emailIndex + 1}`,
|
||||
messageId: `contention-${clientIndex}-${emailIndex}@contention.test`
|
||||
}),
|
||||
clientIndex,
|
||||
emailIndex
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log(' Launching high-contention operations...');
|
||||
const startTime = Date.now();
|
||||
const promises = emails.map(({ client, email, clientIndex, emailIndex }) => {
|
||||
return client.sendMail(email).then(result => {
|
||||
console.log(` ✓ Client ${clientIndex + 1} Email ${emailIndex + 1} sent`);
|
||||
return {
|
||||
success: true,
|
||||
clientIndex,
|
||||
emailIndex,
|
||||
completionTime: Date.now() - startTime
|
||||
};
|
||||
}).catch(error => {
|
||||
console.log(` ✗ Client ${clientIndex + 1} Email ${emailIndex + 1} failed: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
clientIndex,
|
||||
emailIndex,
|
||||
error: error.message,
|
||||
completionTime: Date.now() - startTime
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
const endTime = Date.now();
|
||||
|
||||
// Close all clients
|
||||
clients.forEach(client => client.close());
|
||||
|
||||
const successful = results.filter(r => r.success).length;
|
||||
const failed = results.filter(r => !r.success).length;
|
||||
const avgCompletionTime = results
|
||||
.filter(r => r.success)
|
||||
.reduce((sum, r) => sum + r.completionTime, 0) / successful || 0;
|
||||
|
||||
console.log(`\n Resource contention results:`);
|
||||
console.log(` Total operations: ${emails.length}`);
|
||||
console.log(` Successful: ${successful}, Failed: ${failed}`);
|
||||
console.log(` Success rate: ${((successful / emails.length) * 100).toFixed(1)}%`);
|
||||
console.log(` Total execution time: ${endTime - startTime}ms`);
|
||||
console.log(` Average completion time: ${avgCompletionTime.toFixed(0)}ms`);
|
||||
console.log(` Resource management: ${successful > emails.length * 0.8 ? 'Effective' : 'Needs improvement'}`);
|
||||
|
||||
} finally {
|
||||
testServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Scenario 5: Data Race Prevention
|
||||
await test.test('Scenario 5: Data Race Prevention', async () => {
|
||||
console.log('\n🏃 Testing data race prevention mechanisms...');
|
||||
|
||||
const sharedState = {
|
||||
counter: 0,
|
||||
operations: [] as string[],
|
||||
lock: false
|
||||
};
|
||||
|
||||
const testServer = await createTestServer({
|
||||
responseDelay: 15,
|
||||
onData: (data: string) => {
|
||||
if (data.includes('Data Race Test')) {
|
||||
// Simulate shared state access
|
||||
if (!sharedState.lock) {
|
||||
sharedState.lock = true;
|
||||
sharedState.counter++;
|
||||
sharedState.operations.push(`Operation ${sharedState.counter} at ${Date.now()}`);
|
||||
sharedState.lock = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
console.log(' Setting up concurrent operations that access shared state...');
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 4,
|
||||
racePreventionMode: true,
|
||||
atomicOperations: true
|
||||
});
|
||||
|
||||
const iterations = 20;
|
||||
const concurrentOperations: Promise<any>[] = [];
|
||||
|
||||
console.log(' Launching concurrent operations...');
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const email = new Email({
|
||||
from: 'sender@datarace.test',
|
||||
to: [`recipient${i}@datarace.test`],
|
||||
subject: `Data Race Test ${i + 1}`,
|
||||
text: `Testing data race prevention, operation ${i + 1}`,
|
||||
messageId: `datarace-${i}@datarace.test`
|
||||
});
|
||||
|
||||
const operation = smtpClient.sendMail(email).then(result => {
|
||||
return {
|
||||
success: true,
|
||||
operationId: i + 1,
|
||||
messageId: result.messageId,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}).catch(error => {
|
||||
return {
|
||||
success: false,
|
||||
operationId: i + 1,
|
||||
error: error.message,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
});
|
||||
|
||||
concurrentOperations.push(operation);
|
||||
|
||||
// Add small random delays to increase race condition likelihood
|
||||
if (Math.random() < 0.3) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1));
|
||||
}
|
||||
}
|
||||
|
||||
const results = await Promise.all(concurrentOperations);
|
||||
|
||||
// Wait for shared state operations to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
const successful = results.filter(r => r.success).length;
|
||||
const failed = results.filter(r => !r.success).length;
|
||||
|
||||
console.log(`\n Data race prevention results:`);
|
||||
console.log(` Concurrent operations: ${iterations}`);
|
||||
console.log(` Successful: ${successful}, Failed: ${failed}`);
|
||||
console.log(` Success rate: ${((successful / iterations) * 100).toFixed(1)}%`);
|
||||
console.log(` Shared state counter: ${sharedState.counter}`);
|
||||
console.log(` State operations recorded: ${sharedState.operations.length}`);
|
||||
console.log(` Data consistency: ${sharedState.counter === sharedState.operations.length ? 'Maintained' : 'Race condition detected'}`);
|
||||
console.log(` Race prevention: ${sharedState.counter <= successful ? 'Effective' : 'Needs improvement'}`);
|
||||
|
||||
// Analyze operation timing for race conditions
|
||||
const operationTimes = sharedState.operations.map(op => {
|
||||
const match = op.match(/at (\d+)/);
|
||||
return match ? parseInt(match[1]) : 0;
|
||||
});
|
||||
|
||||
if (operationTimes.length > 1) {
|
||||
const timeGaps = [];
|
||||
for (let i = 1; i < operationTimes.length; i++) {
|
||||
timeGaps.push(operationTimes[i] - operationTimes[i - 1]);
|
||||
}
|
||||
const avgGap = timeGaps.reduce((a, b) => a + b, 0) / timeGaps.length;
|
||||
console.log(` Average operation gap: ${avgGap.toFixed(1)}ms`);
|
||||
console.log(` Timing consistency: ${avgGap > 0 ? 'Sequential' : 'Potential overlap'}`);
|
||||
}
|
||||
|
||||
smtpClient.close();
|
||||
} finally {
|
||||
testServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
console.log('\n✅ CREL-06: Concurrent Operation Safety Reliability Tests completed');
|
||||
console.log('⚡ All concurrency safety scenarios tested successfully');
|
||||
});
|
@ -0,0 +1,585 @@
|
||||
import { test } from '@git.zone/tstest/tapbundle';
|
||||
import { createTestServer, createSmtpClient } from '../../helpers/utils.js';
|
||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
test('CREL-07: Resource Cleanup Reliability Tests', async () => {
|
||||
console.log('\n🧹 Testing SMTP Client Resource Cleanup Reliability');
|
||||
console.log('=' .repeat(60));
|
||||
|
||||
const tempDir = path.join(process.cwd(), '.nogit', 'test-resource-cleanup');
|
||||
|
||||
// Ensure test directory exists
|
||||
if (!fs.existsSync(tempDir)) {
|
||||
fs.mkdirSync(tempDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Helper function to count active resources
|
||||
const getResourceCounts = () => {
|
||||
const usage = process.memoryUsage();
|
||||
return {
|
||||
memory: Math.round(usage.heapUsed / 1024 / 1024 * 100) / 100, // MB
|
||||
handles: process._getActiveHandles ? process._getActiveHandles().length : 0,
|
||||
requests: process._getActiveRequests ? process._getActiveRequests().length : 0
|
||||
};
|
||||
};
|
||||
|
||||
// Scenario 1: Connection Pool Cleanup
|
||||
await test.test('Scenario 1: Connection Pool Cleanup', async () => {
|
||||
console.log('\n🏊 Testing connection pool resource cleanup...');
|
||||
|
||||
let openConnections = 0;
|
||||
let closedConnections = 0;
|
||||
const connectionIds: string[] = [];
|
||||
|
||||
const testServer = await createTestServer({
|
||||
responseDelay: 20,
|
||||
onConnect: (socket: any) => {
|
||||
openConnections++;
|
||||
const connId = `CONN-${openConnections}`;
|
||||
connectionIds.push(connId);
|
||||
console.log(` [Server] ${connId} opened (total open: ${openConnections})`);
|
||||
|
||||
socket.on('close', () => {
|
||||
closedConnections++;
|
||||
console.log(` [Server] Connection closed (total closed: ${closedConnections})`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const initialResources = getResourceCounts();
|
||||
console.log(` Initial resources: ${initialResources.memory}MB memory, ${initialResources.handles} handles`);
|
||||
|
||||
console.log(' Phase 1: Creating and using connection pools...');
|
||||
const clients = [];
|
||||
|
||||
for (let poolIndex = 0; poolIndex < 4; poolIndex++) {
|
||||
console.log(` Creating connection pool ${poolIndex + 1}...`);
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 3,
|
||||
maxMessages: 20,
|
||||
resourceCleanup: true,
|
||||
autoCleanupInterval: 1000
|
||||
});
|
||||
|
||||
clients.push(smtpClient);
|
||||
|
||||
// Send emails through this pool
|
||||
const emails = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
emails.push(new Email({
|
||||
from: `sender${poolIndex}@cleanup.test`,
|
||||
to: [`recipient${i}@cleanup.test`],
|
||||
subject: `Pool Cleanup Test ${poolIndex + 1}-${i + 1}`,
|
||||
text: `Testing connection pool cleanup ${poolIndex + 1}-${i + 1}`,
|
||||
messageId: `pool-cleanup-${poolIndex}-${i}@cleanup.test`
|
||||
}));
|
||||
}
|
||||
|
||||
const promises = emails.map((email, index) => {
|
||||
return smtpClient.sendMail(email).then(result => {
|
||||
console.log(` ✓ Pool ${poolIndex + 1} Email ${index + 1} sent`);
|
||||
return { success: true };
|
||||
}).catch(error => {
|
||||
console.log(` ✗ Pool ${poolIndex + 1} Email ${index + 1} failed`);
|
||||
return { success: false, error };
|
||||
});
|
||||
});
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
const successful = results.filter(r => r.success).length;
|
||||
console.log(` Pool ${poolIndex + 1}: ${successful}/${emails.length} emails sent`);
|
||||
}
|
||||
|
||||
const afterCreation = getResourceCounts();
|
||||
console.log(` After pool creation: ${afterCreation.memory}MB memory, ${afterCreation.handles} handles`);
|
||||
|
||||
console.log(' Phase 2: Closing all pools and testing cleanup...');
|
||||
for (let i = 0; i < clients.length; i++) {
|
||||
console.log(` Closing pool ${i + 1}...`);
|
||||
clients[i].close();
|
||||
|
||||
// Wait for cleanup to occur
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
const currentResources = getResourceCounts();
|
||||
console.log(` Resources after closing pool ${i + 1}: ${currentResources.memory}MB, ${currentResources.handles} handles`);
|
||||
}
|
||||
|
||||
// Wait for all cleanup to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
const finalResources = getResourceCounts();
|
||||
console.log(`\n Resource cleanup assessment:`);
|
||||
console.log(` Initial: ${initialResources.memory}MB memory, ${initialResources.handles} handles`);
|
||||
console.log(` Final: ${finalResources.memory}MB memory, ${finalResources.handles} handles`);
|
||||
console.log(` Memory cleanup: ${finalResources.memory - initialResources.memory < 2 ? 'Good' : 'Memory retained'}`);
|
||||
console.log(` Handle cleanup: ${finalResources.handles <= initialResources.handles + 1 ? 'Good' : 'Handles remaining'}`);
|
||||
console.log(` Connection cleanup: ${closedConnections >= openConnections - 1 ? 'Complete' : 'Incomplete'}`);
|
||||
|
||||
} finally {
|
||||
testServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Scenario 2: File Handle and Stream Cleanup
|
||||
await test.test('Scenario 2: File Handle and Stream Cleanup', async () => {
|
||||
console.log('\n📁 Testing file handle and stream cleanup...');
|
||||
|
||||
const testServer = await createTestServer({
|
||||
responseDelay: 30,
|
||||
onData: (data: string) => {
|
||||
if (data.includes('Attachment Test')) {
|
||||
console.log(' [Server] Processing attachment email');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const initialResources = getResourceCounts();
|
||||
console.log(` Initial resources: ${initialResources.memory}MB memory, ${initialResources.handles} handles`);
|
||||
|
||||
console.log(' Creating temporary files for attachment testing...');
|
||||
const tempFiles: string[] = [];
|
||||
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const fileName = path.join(tempDir, `attachment-${i}.txt`);
|
||||
const content = `Attachment content ${i + 1}\n${'X'.repeat(1000)}`; // 1KB files
|
||||
fs.writeFileSync(fileName, content);
|
||||
tempFiles.push(fileName);
|
||||
console.log(` Created temp file: ${fileName}`);
|
||||
}
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 2,
|
||||
streamCleanup: true,
|
||||
fileHandleManagement: true
|
||||
});
|
||||
|
||||
console.log(' Sending emails with file attachments...');
|
||||
const emailPromises = tempFiles.map((filePath, index) => {
|
||||
const email = new Email({
|
||||
from: 'sender@filehandle.test',
|
||||
to: [`recipient${index}@filehandle.test`],
|
||||
subject: `File Handle Cleanup Test ${index + 1}`,
|
||||
text: `Testing file handle cleanup with attachment ${index + 1}`,
|
||||
attachments: [{
|
||||
filename: `attachment-${index}.txt`,
|
||||
path: filePath
|
||||
}],
|
||||
messageId: `filehandle-${index}@filehandle.test`
|
||||
});
|
||||
|
||||
return smtpClient.sendMail(email).then(result => {
|
||||
console.log(` ✓ Email ${index + 1} with attachment sent`);
|
||||
return { success: true, index };
|
||||
}).catch(error => {
|
||||
console.log(` ✗ Email ${index + 1} failed: ${error.message}`);
|
||||
return { success: false, index, error };
|
||||
});
|
||||
});
|
||||
|
||||
const results = await Promise.all(emailPromises);
|
||||
const successful = results.filter(r => r.success).length;
|
||||
|
||||
console.log(` Email sending completed: ${successful}/${tempFiles.length} successful`);
|
||||
|
||||
const afterSending = getResourceCounts();
|
||||
console.log(` Resources after sending: ${afterSending.memory}MB memory, ${afterSending.handles} handles`);
|
||||
|
||||
console.log(' Closing client and testing file handle cleanup...');
|
||||
smtpClient.close();
|
||||
|
||||
// Wait for cleanup
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
// Clean up temp files
|
||||
tempFiles.forEach(filePath => {
|
||||
try {
|
||||
fs.unlinkSync(filePath);
|
||||
console.log(` Cleaned up: ${filePath}`);
|
||||
} catch (error) {
|
||||
console.log(` Failed to clean up ${filePath}: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
const finalResources = getResourceCounts();
|
||||
console.log(`\n File handle cleanup assessment:`);
|
||||
console.log(` Initial handles: ${initialResources.handles}`);
|
||||
console.log(` After sending: ${afterSending.handles}`);
|
||||
console.log(` Final handles: ${finalResources.handles}`);
|
||||
console.log(` Handle management: ${finalResources.handles <= initialResources.handles + 2 ? 'Effective' : 'Handles leaked'}`);
|
||||
console.log(` Memory cleanup: ${finalResources.memory - initialResources.memory < 3 ? 'Good' : 'Memory retained'}`);
|
||||
|
||||
} finally {
|
||||
testServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Scenario 3: Timer and Interval Cleanup
|
||||
await test.test('Scenario 3: Timer and Interval Cleanup', async () => {
|
||||
console.log('\n⏰ Testing timer and interval cleanup...');
|
||||
|
||||
const testServer = await createTestServer({
|
||||
responseDelay: 40
|
||||
});
|
||||
|
||||
try {
|
||||
const initialResources = getResourceCounts();
|
||||
console.log(` Initial resources: ${initialResources.memory}MB memory, ${initialResources.handles} handles`);
|
||||
|
||||
console.log(' Creating clients with various timer configurations...');
|
||||
const clients = [];
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 2,
|
||||
// Timer configurations
|
||||
connectionTimeout: 2000,
|
||||
keepAlive: true,
|
||||
keepAliveInterval: 1000,
|
||||
retryDelay: 500,
|
||||
healthCheckInterval: 800,
|
||||
timerCleanup: true
|
||||
});
|
||||
|
||||
clients.push(smtpClient);
|
||||
|
||||
// Send an email to activate timers
|
||||
const email = new Email({
|
||||
from: `sender${i}@timer.test`,
|
||||
to: [`recipient${i}@timer.test`],
|
||||
subject: `Timer Cleanup Test ${i + 1}`,
|
||||
text: `Testing timer cleanup ${i + 1}`,
|
||||
messageId: `timer-${i}@timer.test`
|
||||
});
|
||||
|
||||
try {
|
||||
await smtpClient.sendMail(email);
|
||||
console.log(` ✓ Client ${i + 1} email sent (timers active)`);
|
||||
} catch (error) {
|
||||
console.log(` ✗ Client ${i + 1} failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Let timers run for a while
|
||||
console.log(' Allowing timers to run...');
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
const withTimers = getResourceCounts();
|
||||
console.log(` Resources with active timers: ${withTimers.memory}MB memory, ${withTimers.handles} handles`);
|
||||
|
||||
console.log(' Closing clients and testing timer cleanup...');
|
||||
for (let i = 0; i < clients.length; i++) {
|
||||
console.log(` Closing client ${i + 1}...`);
|
||||
clients[i].close();
|
||||
|
||||
// Wait for timer cleanup
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
|
||||
const currentResources = getResourceCounts();
|
||||
console.log(` Resources after closing client ${i + 1}: ${currentResources.handles} handles`);
|
||||
}
|
||||
|
||||
// Wait for all timer cleanup to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
|
||||
const finalResources = getResourceCounts();
|
||||
console.log(`\n Timer cleanup assessment:`);
|
||||
console.log(` Initial handles: ${initialResources.handles}`);
|
||||
console.log(` With timers: ${withTimers.handles}`);
|
||||
console.log(` Final handles: ${finalResources.handles}`);
|
||||
console.log(` Timer cleanup: ${finalResources.handles <= initialResources.handles + 1 ? 'Complete' : 'Timers remaining'}`);
|
||||
console.log(` Resource management: ${finalResources.handles < withTimers.handles ? 'Effective' : 'Incomplete'}`);
|
||||
|
||||
} finally {
|
||||
testServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Scenario 4: Event Listener and Callback Cleanup
|
||||
await test.test('Scenario 4: Event Listener and Callback Cleanup', async () => {
|
||||
console.log('\n🎧 Testing event listener and callback cleanup...');
|
||||
|
||||
const testServer = await createTestServer({
|
||||
responseDelay: 25,
|
||||
onConnect: () => {
|
||||
console.log(' [Server] Connection for event cleanup test');
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const initialResources = getResourceCounts();
|
||||
console.log(` Initial resources: ${initialResources.memory}MB memory`);
|
||||
|
||||
console.log(' Creating clients with extensive event listeners...');
|
||||
const clients = [];
|
||||
const eventHandlers: any[] = [];
|
||||
|
||||
for (let clientIndex = 0; clientIndex < 5; clientIndex++) {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 1,
|
||||
eventCleanup: true
|
||||
});
|
||||
|
||||
clients.push(smtpClient);
|
||||
|
||||
// Add multiple event listeners
|
||||
const handlers = [];
|
||||
for (let eventIndex = 0; eventIndex < 6; eventIndex++) {
|
||||
const handler = (data: any) => {
|
||||
// Event handler with closure
|
||||
console.log(` Event ${eventIndex} from client ${clientIndex}: ${typeof data}`);
|
||||
};
|
||||
handlers.push(handler);
|
||||
|
||||
// Add event listeners (simulated)
|
||||
if (smtpClient.on) {
|
||||
smtpClient.on('connect', handler);
|
||||
smtpClient.on('error', handler);
|
||||
smtpClient.on('close', handler);
|
||||
smtpClient.on('data', handler);
|
||||
}
|
||||
}
|
||||
eventHandlers.push(handlers);
|
||||
|
||||
// Send test email to trigger events
|
||||
const email = new Email({
|
||||
from: `sender${clientIndex}@eventcleanup.test`,
|
||||
to: [`recipient${clientIndex}@eventcleanup.test`],
|
||||
subject: `Event Cleanup Test ${clientIndex + 1}`,
|
||||
text: `Testing event listener cleanup ${clientIndex + 1}`,
|
||||
messageId: `event-cleanup-${clientIndex}@eventcleanup.test`
|
||||
});
|
||||
|
||||
try {
|
||||
await smtpClient.sendMail(email);
|
||||
console.log(` ✓ Client ${clientIndex + 1} email sent (events active)`);
|
||||
} catch (error) {
|
||||
console.log(` ✗ Client ${clientIndex + 1} failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const withEvents = getResourceCounts();
|
||||
console.log(` Resources with event listeners: ${withEvents.memory}MB memory`);
|
||||
|
||||
console.log(' Closing clients and testing event listener cleanup...');
|
||||
for (let i = 0; i < clients.length; i++) {
|
||||
console.log(` Closing client ${i + 1} and removing ${eventHandlers[i].length} event listeners...`);
|
||||
|
||||
// Remove event listeners manually first
|
||||
if (clients[i].removeAllListeners) {
|
||||
clients[i].removeAllListeners();
|
||||
}
|
||||
|
||||
// Close client
|
||||
clients[i].close();
|
||||
|
||||
// Clear handler references
|
||||
eventHandlers[i].length = 0;
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
// Force garbage collection if available
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
global.gc();
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
const finalResources = getResourceCounts();
|
||||
console.log(`\n Event listener cleanup assessment:`);
|
||||
console.log(` Initial memory: ${initialResources.memory}MB`);
|
||||
console.log(` With events: ${withEvents.memory}MB`);
|
||||
console.log(` Final memory: ${finalResources.memory}MB`);
|
||||
console.log(` Memory cleanup: ${finalResources.memory - initialResources.memory < 2 ? 'Effective' : 'Memory retained'}`);
|
||||
console.log(` Event cleanup: ${finalResources.memory < withEvents.memory ? 'Successful' : 'Partial'}`);
|
||||
|
||||
} finally {
|
||||
testServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Scenario 5: Error State Cleanup
|
||||
await test.test('Scenario 5: Error State Cleanup', async () => {
|
||||
console.log('\n💥 Testing error state cleanup...');
|
||||
|
||||
let connectionCount = 0;
|
||||
let errorInjectionActive = false;
|
||||
|
||||
const testServer = await createTestServer({
|
||||
responseDelay: 30,
|
||||
onConnect: (socket: any) => {
|
||||
connectionCount++;
|
||||
console.log(` [Server] Connection ${connectionCount}`);
|
||||
|
||||
if (errorInjectionActive && connectionCount > 2) {
|
||||
console.log(` [Server] Injecting connection error ${connectionCount}`);
|
||||
setTimeout(() => socket.destroy(), 50);
|
||||
}
|
||||
},
|
||||
onData: (data: string, socket: any) => {
|
||||
if (errorInjectionActive && data.includes('MAIL FROM')) {
|
||||
socket.write('500 Internal server error\r\n');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const initialResources = getResourceCounts();
|
||||
console.log(` Initial resources: ${initialResources.memory}MB memory, ${initialResources.handles} handles`);
|
||||
|
||||
console.log(' Creating clients for error state testing...');
|
||||
const clients = [];
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
clients.push(createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 2,
|
||||
retryDelay: 200,
|
||||
retries: 2,
|
||||
errorStateCleanup: true,
|
||||
gracefulErrorHandling: true
|
||||
}));
|
||||
}
|
||||
|
||||
console.log(' Phase 1: Normal operation...');
|
||||
const email1 = new Email({
|
||||
from: 'sender@errorstate.test',
|
||||
to: ['recipient1@errorstate.test'],
|
||||
subject: 'Error State Test - Normal',
|
||||
text: 'Normal operation before errors',
|
||||
messageId: 'error-normal@errorstate.test'
|
||||
});
|
||||
|
||||
try {
|
||||
await clients[0].sendMail(email1);
|
||||
console.log(' ✓ Normal operation successful');
|
||||
} catch (error) {
|
||||
console.log(' ✗ Normal operation failed');
|
||||
}
|
||||
|
||||
const afterNormal = getResourceCounts();
|
||||
console.log(` Resources after normal operation: ${afterNormal.handles} handles`);
|
||||
|
||||
console.log(' Phase 2: Error injection phase...');
|
||||
errorInjectionActive = true;
|
||||
|
||||
const errorEmails = [];
|
||||
for (let i = 0; i < 6; i++) {
|
||||
errorEmails.push(new Email({
|
||||
from: 'sender@errorstate.test',
|
||||
to: [`recipient${i}@errorstate.test`],
|
||||
subject: `Error State Test ${i + 1}`,
|
||||
text: `Testing error state cleanup ${i + 1}`,
|
||||
messageId: `error-state-${i}@errorstate.test`
|
||||
}));
|
||||
}
|
||||
|
||||
const errorPromises = errorEmails.map((email, index) => {
|
||||
const client = clients[index % clients.length];
|
||||
return client.sendMail(email).then(result => {
|
||||
console.log(` ✓ Error email ${index + 1} unexpectedly succeeded`);
|
||||
return { success: true, index };
|
||||
}).catch(error => {
|
||||
console.log(` ✗ Error email ${index + 1} failed as expected`);
|
||||
return { success: false, index, error: error.message };
|
||||
});
|
||||
});
|
||||
|
||||
const errorResults = await Promise.all(errorPromises);
|
||||
const afterErrors = getResourceCounts();
|
||||
console.log(` Resources after error phase: ${afterErrors.handles} handles`);
|
||||
|
||||
console.log(' Phase 3: Recovery and cleanup...');
|
||||
errorInjectionActive = false;
|
||||
|
||||
// Test recovery
|
||||
const recoveryEmail = new Email({
|
||||
from: 'sender@errorstate.test',
|
||||
to: ['recovery@errorstate.test'],
|
||||
subject: 'Error State Test - Recovery',
|
||||
text: 'Testing recovery after errors',
|
||||
messageId: 'error-recovery@errorstate.test'
|
||||
});
|
||||
|
||||
try {
|
||||
await clients[0].sendMail(recoveryEmail);
|
||||
console.log(' ✓ Recovery successful');
|
||||
} catch (error) {
|
||||
console.log(' ✗ Recovery failed');
|
||||
}
|
||||
|
||||
console.log(' Closing all clients...');
|
||||
clients.forEach((client, index) => {
|
||||
console.log(` Closing client ${index + 1}...`);
|
||||
client.close();
|
||||
});
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
const finalResources = getResourceCounts();
|
||||
|
||||
const errorSuccessful = errorResults.filter(r => r.success).length;
|
||||
const errorFailed = errorResults.filter(r => !r.success).length;
|
||||
|
||||
console.log(`\n Error state cleanup assessment:`);
|
||||
console.log(` Error phase results: ${errorSuccessful} succeeded, ${errorFailed} failed`);
|
||||
console.log(` Initial handles: ${initialResources.handles}`);
|
||||
console.log(` After errors: ${afterErrors.handles}`);
|
||||
console.log(` Final handles: ${finalResources.handles}`);
|
||||
console.log(` Error state cleanup: ${finalResources.handles <= initialResources.handles + 1 ? 'Complete' : 'Incomplete'}`);
|
||||
console.log(` Recovery capability: ${errorFailed > 0 ? 'Error handling active' : 'No errors detected'}`);
|
||||
console.log(` Resource management: ${finalResources.handles < afterErrors.handles ? 'Effective' : 'Needs improvement'}`);
|
||||
|
||||
} finally {
|
||||
testServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Cleanup test directory
|
||||
try {
|
||||
if (fs.existsSync(tempDir)) {
|
||||
const files = fs.readdirSync(tempDir);
|
||||
for (const file of files) {
|
||||
const filePath = path.join(tempDir, file);
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
}
|
||||
fs.rmdirSync(tempDir);
|
||||
console.log('\n🧹 Test directory cleaned up successfully');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`\n⚠️ Warning: Could not clean up test directory: ${error.message}`);
|
||||
}
|
||||
|
||||
console.log('\n✅ CREL-07: Resource Cleanup Reliability Tests completed');
|
||||
console.log('🧹 All resource cleanup scenarios tested successfully');
|
||||
});
|
@ -0,0 +1,522 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as plugins from './plugins.js';
|
||||
import { createTestServer } from '../../helpers/server.loader.js';
|
||||
import { createSmtpClient } from '../../helpers/smtp.client.js';
|
||||
|
||||
tap.test('CRFC-03: should comply with SMTP command syntax (RFC 5321)', async (tools) => {
|
||||
const testId = 'CRFC-03-command-syntax';
|
||||
console.log(`\n${testId}: Testing SMTP command syntax compliance...`);
|
||||
|
||||
let scenarioCount = 0;
|
||||
|
||||
// Scenario 1: EHLO/HELO command syntax
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing EHLO/HELO command syntax`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 syntax.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.match(/^EHLO\s+[^\s]+$/i)) {
|
||||
const domain = command.split(' ')[1];
|
||||
console.log(` [Server] Valid EHLO with domain: ${domain}`);
|
||||
|
||||
// Validate domain format (basic check)
|
||||
if (domain.includes('.') || domain === 'localhost' || domain.match(/^\[[\d\.]+\]$/)) {
|
||||
socket.write('250-syntax.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else {
|
||||
socket.write('501 5.5.4 Invalid domain name\r\n');
|
||||
}
|
||||
} else if (command.match(/^HELO\s+[^\s]+$/i)) {
|
||||
const domain = command.split(' ')[1];
|
||||
console.log(` [Server] Valid HELO with domain: ${domain}`);
|
||||
socket.write('250 syntax.example.com\r\n');
|
||||
} else if (command === 'EHLO' || command === 'HELO') {
|
||||
console.log(' [Server] Missing domain parameter');
|
||||
socket.write('501 5.5.4 EHLO/HELO requires domain name\r\n');
|
||||
} else if (command.startsWith('EHLO ') && command.split(' ').length > 2) {
|
||||
console.log(' [Server] Too many parameters');
|
||||
socket.write('501 5.5.4 EHLO syntax error\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
} else {
|
||||
socket.write('500 5.5.1 Command not recognized\r\n');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
name: 'client.example.com' // Valid domain
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'EHLO syntax test',
|
||||
text: 'Testing proper EHLO syntax'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(' Valid EHLO syntax accepted');
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 2: MAIL FROM command syntax
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing MAIL FROM command syntax`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 syntax.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-syntax.example.com\r\n');
|
||||
socket.write('250-SIZE 10485760\r\n');
|
||||
socket.write('250-8BITMIME\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.match(/^MAIL FROM:\s*<[^>]*>(\s+[A-Z0-9]+=\S*)*\s*$/i)) {
|
||||
// Valid MAIL FROM syntax with optional parameters
|
||||
const address = command.match(/<([^>]*)>/)?.[1] || '';
|
||||
console.log(` [Server] Valid MAIL FROM: ${address}`);
|
||||
|
||||
// Validate email address format
|
||||
if (address === '' || address.includes('@') || address === 'postmaster') {
|
||||
// Check for ESMTP parameters
|
||||
const params = command.substring(command.indexOf('>') + 1).trim();
|
||||
if (params) {
|
||||
console.log(` [Server] ESMTP parameters: ${params}`);
|
||||
|
||||
// Validate parameter syntax
|
||||
const validParams = /^(\s+[A-Z0-9]+=\S*)*\s*$/i.test(params);
|
||||
if (validParams) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else {
|
||||
socket.write('501 5.5.4 Invalid MAIL FROM parameters\r\n');
|
||||
}
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else {
|
||||
socket.write('553 5.1.8 Invalid sender address\r\n');
|
||||
}
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
console.log(' [Server] Invalid MAIL FROM syntax');
|
||||
if (!command.includes('<') || !command.includes('>')) {
|
||||
socket.write('501 5.5.4 MAIL FROM requires <address>\r\n');
|
||||
} else {
|
||||
socket.write('501 5.5.4 Syntax error in MAIL FROM\r\n');
|
||||
}
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Test with various sender formats
|
||||
const testCases = [
|
||||
{ from: 'sender@example.com', desc: 'normal address' },
|
||||
{ from: '', desc: 'null sender (bounce)' },
|
||||
{ from: 'postmaster', desc: 'postmaster without domain' },
|
||||
{ from: 'user+tag@example.com', desc: 'address with plus extension' }
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
console.log(` Testing ${testCase.desc}...`);
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: testCase.from || 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: `MAIL FROM syntax test: ${testCase.desc}`,
|
||||
text: `Testing MAIL FROM with ${testCase.desc}`
|
||||
});
|
||||
|
||||
// For null sender, modify the envelope
|
||||
if (testCase.from === '') {
|
||||
email.envelope = { from: '', to: ['recipient@example.com'] };
|
||||
}
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
expect(result).toBeDefined();
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 3: RCPT TO command syntax
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing RCPT TO command syntax`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 syntax.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-syntax.example.com\r\n');
|
||||
socket.write('250-DSN\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.match(/^RCPT TO:\s*<[^>]*>(\s+[A-Z0-9]+=\S*)*\s*$/i)) {
|
||||
// Valid RCPT TO syntax with optional parameters
|
||||
const address = command.match(/<([^>]*)>/)?.[1] || '';
|
||||
console.log(` [Server] Valid RCPT TO: ${address}`);
|
||||
|
||||
// Validate recipient address
|
||||
if (address.includes('@') && address.split('@').length === 2) {
|
||||
// Check for DSN parameters
|
||||
const params = command.substring(command.indexOf('>') + 1).trim();
|
||||
if (params) {
|
||||
console.log(` [Server] DSN parameters: ${params}`);
|
||||
|
||||
// Validate NOTIFY and ORCPT parameters
|
||||
if (params.includes('NOTIFY=') || params.includes('ORCPT=')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else {
|
||||
socket.write('501 5.5.4 Invalid RCPT TO parameters\r\n');
|
||||
}
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else {
|
||||
socket.write('553 5.1.3 Invalid recipient address\r\n');
|
||||
}
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
console.log(' [Server] Invalid RCPT TO syntax');
|
||||
if (!command.includes('<') || !command.includes('>')) {
|
||||
socket.write('501 5.5.4 RCPT TO requires <address>\r\n');
|
||||
} else {
|
||||
socket.write('501 5.5.4 Syntax error in RCPT TO\r\n');
|
||||
}
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Test with various recipient formats
|
||||
const recipients = [
|
||||
'user@example.com',
|
||||
'user.name@example.com',
|
||||
'user+tag@example.com',
|
||||
'user_name@sub.example.com'
|
||||
];
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: recipients,
|
||||
subject: 'RCPT TO syntax test',
|
||||
text: 'Testing RCPT TO command syntax'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(` Valid RCPT TO syntax for ${recipients.length} recipients`);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.accepted?.length).toBe(recipients.length);
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 4: DATA command and message termination
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing DATA command and message termination`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 syntax.example.com ESMTP\r\n');
|
||||
|
||||
let inDataMode = false;
|
||||
let messageData = '';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
if (inDataMode) {
|
||||
messageData += data.toString();
|
||||
|
||||
// Check for proper message termination
|
||||
if (messageData.includes('\r\n.\r\n')) {
|
||||
inDataMode = false;
|
||||
console.log(' [Server] Message terminated with CRLF.CRLF');
|
||||
|
||||
// Check for transparency (dot stuffing)
|
||||
const lines = messageData.split('\r\n');
|
||||
let hasDotStuffing = false;
|
||||
lines.forEach(line => {
|
||||
if (line.startsWith('..')) {
|
||||
hasDotStuffing = true;
|
||||
console.log(' [Server] Found dot stuffing in line');
|
||||
}
|
||||
});
|
||||
|
||||
socket.write('250 OK: Message accepted\r\n');
|
||||
messageData = '';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-syntax.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
console.log(' [Server] Entering DATA mode');
|
||||
socket.write('354 Start mail input; end with <CRLF>.<CRLF>\r\n');
|
||||
inDataMode = true;
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Test with message containing dots at line start (transparency test)
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'DATA transparency test',
|
||||
text: 'Line 1\n.This line starts with a dot\n..This line starts with two dots\nLine 4'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(' DATA command and transparency handled correctly');
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 5: RSET command syntax
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing RSET command syntax`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 syntax.example.com ESMTP\r\n');
|
||||
|
||||
let transactionState = 'initial';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command} (state: ${transactionState})`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-syntax.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
transactionState = 'ready';
|
||||
} else if (command.startsWith('MAIL FROM:') && transactionState === 'ready') {
|
||||
socket.write('250 OK\r\n');
|
||||
transactionState = 'mail';
|
||||
} else if (command.startsWith('RCPT TO:') && transactionState === 'mail') {
|
||||
socket.write('250 OK\r\n');
|
||||
transactionState = 'rcpt';
|
||||
} else if (command === 'RSET') {
|
||||
console.log(' [Server] RSET - resetting transaction state');
|
||||
socket.write('250 OK\r\n');
|
||||
transactionState = 'ready';
|
||||
} else if (command.match(/^RSET\s+/)) {
|
||||
console.log(' [Server] RSET with parameters - syntax error');
|
||||
socket.write('501 5.5.4 RSET does not accept parameters\r\n');
|
||||
} else if (command === 'DATA' && transactionState === 'rcpt') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
transactionState = 'ready';
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
} else {
|
||||
socket.write('503 5.5.1 Bad sequence of commands\r\n');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Start a transaction then reset it
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'RSET test',
|
||||
text: 'Testing RSET command'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(' RSET command syntax validated');
|
||||
expect(result).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 6: Command line length limits
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing command line length limits`);
|
||||
|
||||
const maxLineLength = 512; // RFC 5321 limit
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 syntax.example.com ESMTP\r\n');
|
||||
|
||||
let lineBuffer = '';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
lineBuffer += data.toString();
|
||||
|
||||
const lines = lineBuffer.split('\r\n');
|
||||
lineBuffer = lines.pop() || ''; // Keep incomplete line
|
||||
|
||||
lines.forEach(line => {
|
||||
if (line.length === 0) return;
|
||||
|
||||
console.log(` [Server] Line length: ${line.length} chars`);
|
||||
|
||||
if (line.length > maxLineLength) {
|
||||
console.log(' [Server] Line too long');
|
||||
socket.write('500 5.5.1 Line too long\r\n');
|
||||
return;
|
||||
}
|
||||
|
||||
if (line.startsWith('EHLO')) {
|
||||
socket.write('250-syntax.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (line === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Test with normal length commands
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Line length test',
|
||||
text: 'Testing command line length limits'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(' Normal command lengths accepted');
|
||||
expect(result).toBeDefined();
|
||||
|
||||
// Test with very long recipient address
|
||||
const longRecipient = 'very-long-username-that-exceeds-normal-limits@' + 'x'.repeat(400) + '.com';
|
||||
|
||||
const longEmail = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: [longRecipient],
|
||||
subject: 'Long recipient test',
|
||||
text: 'Testing very long recipient address'
|
||||
});
|
||||
|
||||
try {
|
||||
await smtpClient.sendMail(longEmail);
|
||||
console.log(' Long command handled (possibly folded)');
|
||||
} catch (error) {
|
||||
console.log(' Long command rejected as expected');
|
||||
expect(error.message).toContain('too long');
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
console.log(`\n${testId}: All ${scenarioCount} command syntax scenarios tested ✓`);
|
||||
});
|
@ -0,0 +1,511 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as plugins from './plugins.js';
|
||||
import { createTestServer } from '../../helpers/server.loader.js';
|
||||
import { createSmtpClient } from '../../helpers/smtp.client.js';
|
||||
|
||||
tap.test('CRFC-04: should handle SMTP response codes correctly (RFC 5321)', async (tools) => {
|
||||
const testId = 'CRFC-04-response-codes';
|
||||
console.log(`\n${testId}: Testing SMTP response code compliance...`);
|
||||
|
||||
let scenarioCount = 0;
|
||||
|
||||
// Scenario 1: 2xx success response codes
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing 2xx success response codes`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 responses.example.com Service ready\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
// 250 - Requested mail action okay, completed
|
||||
socket.write('250-responses.example.com\r\n');
|
||||
socket.write('250-SIZE 10485760\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
// 250 - Requested mail action okay, completed
|
||||
socket.write('250 2.1.0 Sender OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
// 250 - Requested mail action okay, completed
|
||||
socket.write('250 2.1.5 Recipient OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
// 354 - Start mail input; end with <CRLF>.<CRLF>
|
||||
socket.write('354 Start mail input; end with <CRLF>.<CRLF>\r\n');
|
||||
} else if (command === '.') {
|
||||
// 250 - Requested mail action okay, completed
|
||||
socket.write('250 2.0.0 Message accepted for delivery\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
// 221 - Service closing transmission channel
|
||||
socket.write('221 2.0.0 Service closing transmission channel\r\n');
|
||||
socket.end();
|
||||
} else if (command === 'NOOP') {
|
||||
// 250 - Requested mail action okay, completed
|
||||
socket.write('250 2.0.0 OK\r\n');
|
||||
} else if (command === 'RSET') {
|
||||
// 250 - Requested mail action okay, completed
|
||||
socket.write('250 2.0.0 Reset OK\r\n');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: '2xx response test',
|
||||
text: 'Testing 2xx success response codes'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(' All 2xx success codes handled correctly');
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 2: 4xx temporary failure response codes
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing 4xx temporary failure response codes`);
|
||||
|
||||
let attemptCount = 0;
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
attemptCount++;
|
||||
console.log(` [Server] Client connected (attempt ${attemptCount})`);
|
||||
socket.write('220 responses.example.com Service ready\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-responses.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
if (attemptCount === 1) {
|
||||
// 451 - Requested action aborted: local error in processing
|
||||
socket.write('451 4.3.0 Temporary system failure, try again later\r\n');
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
const address = command.match(/<(.+)>/)?.[1] || '';
|
||||
|
||||
if (address.includes('full')) {
|
||||
// 452 - Requested action not taken: insufficient system storage
|
||||
socket.write('452 4.2.2 Mailbox full, try again later\r\n');
|
||||
} else if (address.includes('busy')) {
|
||||
// 450 - Requested mail action not taken: mailbox unavailable
|
||||
socket.write('450 4.2.1 Mailbox busy, try again later\r\n');
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
if (attemptCount === 1) {
|
||||
// 421 - Service not available, closing transmission channel
|
||||
socket.write('421 4.3.2 System shutting down, try again later\r\n');
|
||||
socket.end();
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Test temporary failures with retry
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// First attempt with temporary failure
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: '4xx response test',
|
||||
text: 'Testing 4xx temporary failure codes'
|
||||
});
|
||||
|
||||
try {
|
||||
await smtpClient.sendMail(email);
|
||||
console.log(' Unexpected: First attempt succeeded');
|
||||
} catch (error) {
|
||||
console.log(' Expected: Temporary failure on first attempt');
|
||||
expect(error.responseCode).toBeGreaterThanOrEqual(400);
|
||||
expect(error.responseCode).toBeLessThan(500);
|
||||
}
|
||||
|
||||
// Second attempt should succeed
|
||||
const retryResult = await smtpClient.sendMail(email);
|
||||
console.log(' Retry after temporary failure succeeded');
|
||||
expect(retryResult).toBeDefined();
|
||||
|
||||
// Test specific 4xx codes
|
||||
const tempFailureEmail = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['full@example.com', 'busy@example.com'],
|
||||
subject: 'Specific 4xx test',
|
||||
text: 'Testing specific temporary failure codes'
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await smtpClient.sendMail(tempFailureEmail);
|
||||
console.log(` Partial delivery: ${result.rejected?.length || 0} rejected`);
|
||||
expect(result.rejected?.length).toBeGreaterThan(0);
|
||||
} catch (error) {
|
||||
console.log(' Multiple 4xx failures handled');
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 3: 5xx permanent failure response codes
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing 5xx permanent failure response codes`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 responses.example.com Service ready\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-responses.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
const address = command.match(/<(.+)>/)?.[1] || '';
|
||||
|
||||
if (address.includes('blocked')) {
|
||||
// 550 - Requested action not taken: mailbox unavailable
|
||||
socket.write('550 5.1.1 Sender blocked\r\n');
|
||||
} else if (address.includes('invalid')) {
|
||||
// 553 - Requested action not taken: mailbox name not allowed
|
||||
socket.write('553 5.1.8 Invalid sender address format\r\n');
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
const address = command.match(/<(.+)>/)?.[1] || '';
|
||||
|
||||
if (address.includes('unknown')) {
|
||||
// 550 - Requested action not taken: mailbox unavailable
|
||||
socket.write('550 5.1.1 User unknown\r\n');
|
||||
} else if (address.includes('disabled')) {
|
||||
// 551 - User not local; please try <forward-path>
|
||||
socket.write('551 5.1.6 User account disabled\r\n');
|
||||
} else if (address.includes('relay')) {
|
||||
// 554 - Transaction failed
|
||||
socket.write('554 5.7.1 Relay access denied\r\n');
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
} else if (command.startsWith('INVALID')) {
|
||||
// 500 - Syntax error, command unrecognized
|
||||
socket.write('500 5.5.1 Command not recognized\r\n');
|
||||
} else if (command === 'MAIL') {
|
||||
// 501 - Syntax error in parameters or arguments
|
||||
socket.write('501 5.5.4 Syntax error in MAIL command\r\n');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Test various 5xx permanent failures
|
||||
const testCases = [
|
||||
{ from: 'blocked@example.com', to: 'recipient@example.com', desc: 'blocked sender' },
|
||||
{ from: 'sender@example.com', to: 'unknown@example.com', desc: 'unknown recipient' },
|
||||
{ from: 'sender@example.com', to: 'disabled@example.com', desc: 'disabled user' },
|
||||
{ from: 'sender@example.com', to: 'relay@external.com', desc: 'relay denied' }
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
console.log(` Testing ${testCase.desc}...`);
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: testCase.from,
|
||||
to: [testCase.to],
|
||||
subject: `5xx test: ${testCase.desc}`,
|
||||
text: `Testing 5xx permanent failure: ${testCase.desc}`
|
||||
});
|
||||
|
||||
try {
|
||||
await smtpClient.sendMail(email);
|
||||
console.log(` Unexpected: ${testCase.desc} succeeded`);
|
||||
} catch (error) {
|
||||
console.log(` Expected: ${testCase.desc} failed with 5xx`);
|
||||
expect(error.responseCode).toBeGreaterThanOrEqual(500);
|
||||
expect(error.responseCode).toBeLessThan(600);
|
||||
}
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 4: Multi-line response handling
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing multi-line response handling`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220-responses.example.com ESMTP Service Ready\r\n');
|
||||
socket.write('220-This server supports multiple extensions\r\n');
|
||||
socket.write('220 Please proceed with EHLO\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
// Multi-line EHLO response
|
||||
socket.write('250-responses.example.com Hello client\r\n');
|
||||
socket.write('250-SIZE 10485760\r\n');
|
||||
socket.write('250-8BITMIME\r\n');
|
||||
socket.write('250-STARTTLS\r\n');
|
||||
socket.write('250-ENHANCEDSTATUSCODES\r\n');
|
||||
socket.write('250-PIPELINING\r\n');
|
||||
socket.write('250-DSN\r\n');
|
||||
socket.write('250 HELP\r\n'); // Last line ends with space
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input; end with <CRLF>.<CRLF>\r\n');
|
||||
} else if (command === '.') {
|
||||
// Multi-line success response
|
||||
socket.write('250-Message accepted for delivery\r\n');
|
||||
socket.write('250-Queue ID: ABC123\r\n');
|
||||
socket.write('250 Thank you\r\n');
|
||||
} else if (command === 'HELP') {
|
||||
// Multi-line help response
|
||||
socket.write('214-This server supports the following commands:\r\n');
|
||||
socket.write('214-EHLO HELO MAIL RCPT DATA\r\n');
|
||||
socket.write('214-RSET NOOP QUIT HELP\r\n');
|
||||
socket.write('214 For more info visit http://example.com/help\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221-Thank you for using our service\r\n');
|
||||
socket.write('221 Goodbye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Multi-line response test',
|
||||
text: 'Testing multi-line SMTP response handling'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(' Multi-line responses handled correctly');
|
||||
expect(result).toBeDefined();
|
||||
expect(result.response).toContain('Queue ID');
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 5: Response code format validation
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing response code format validation`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 responses.example.com ESMTP\r\n');
|
||||
|
||||
let commandCount = 0;
|
||||
|
||||
socket.on('data', (data) => {
|
||||
commandCount++;
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-responses.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
// Test various response code formats
|
||||
if (commandCount === 2) {
|
||||
// Valid 3-digit code
|
||||
socket.write('250 OK\r\n');
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
const address = command.match(/<(.+)>/)?.[1] || '';
|
||||
|
||||
if (address.includes('enhanced')) {
|
||||
// Enhanced status code format (RFC 3463)
|
||||
socket.write('250 2.1.5 Recipient OK\r\n');
|
||||
} else if (address.includes('detailed')) {
|
||||
// Detailed response with explanation
|
||||
socket.write('250 OK: Recipient accepted for delivery to local mailbox\r\n');
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input; end with <CRLF>.<CRLF>\r\n');
|
||||
} else if (command === '.') {
|
||||
// Response with timestamp
|
||||
const timestamp = new Date().toISOString();
|
||||
socket.write(`250 OK: Message accepted at ${timestamp}\r\n`);
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Service closing transmission channel\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Test with recipients that trigger different response formats
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['enhanced@example.com', 'detailed@example.com', 'normal@example.com'],
|
||||
subject: 'Response format test',
|
||||
text: 'Testing SMTP response code format compliance'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(' Various response code formats handled');
|
||||
expect(result).toBeDefined();
|
||||
expect(result.response).toContain('Message accepted');
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 6: Error recovery and continuation
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing error recovery and continuation`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 responses.example.com ESMTP\r\n');
|
||||
|
||||
let errorCount = 0;
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-responses.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
const address = command.match(/<(.+)>/)?.[1] || '';
|
||||
|
||||
if (address.includes('error1')) {
|
||||
errorCount++;
|
||||
socket.write('550 5.1.1 First error - user unknown\r\n');
|
||||
} else if (address.includes('error2')) {
|
||||
errorCount++;
|
||||
socket.write('551 5.1.6 Second error - user not local\r\n');
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else if (command === 'DATA') {
|
||||
if (errorCount > 0) {
|
||||
console.log(` [Server] ${errorCount} errors occurred, but continuing`);
|
||||
}
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write(`250 OK: Message accepted despite ${errorCount} recipient errors\r\n`);
|
||||
} else if (command === 'RSET') {
|
||||
console.log(' [Server] Transaction reset');
|
||||
errorCount = 0;
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
} else {
|
||||
// Unknown command
|
||||
socket.write('500 5.5.1 Command not recognized\r\n');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Test with mix of valid and invalid recipients
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['error1@example.com', 'valid@example.com', 'error2@example.com', 'another-valid@example.com'],
|
||||
subject: 'Error recovery test',
|
||||
text: 'Testing error handling and recovery'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(` Partial delivery: ${result.accepted?.length || 0} accepted, ${result.rejected?.length || 0} rejected`);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.accepted?.length).toBeGreaterThan(0);
|
||||
expect(result.rejected?.length).toBeGreaterThan(0);
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
console.log(`\n${testId}: All ${scenarioCount} response code scenarios tested ✓`);
|
||||
});
|
@ -0,0 +1,701 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as plugins from './plugins.js';
|
||||
import { createTestServer } from '../../helpers/server.loader.js';
|
||||
import { createSmtpClient } from '../../helpers/smtp.client.js';
|
||||
|
||||
tap.test('CRFC-05: should comply with SMTP state machine (RFC 5321)', async (tools) => {
|
||||
const testId = 'CRFC-05-state-machine';
|
||||
console.log(`\n${testId}: Testing SMTP state machine compliance...`);
|
||||
|
||||
let scenarioCount = 0;
|
||||
|
||||
// Scenario 1: Initial state and greeting
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing initial state and greeting`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected - Initial state');
|
||||
|
||||
let state = 'initial';
|
||||
|
||||
// Send greeting immediately upon connection
|
||||
socket.write('220 statemachine.example.com ESMTP Service ready\r\n');
|
||||
state = 'greeting-sent';
|
||||
console.log(' [Server] State: initial -> greeting-sent');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] State: ${state}, Received: ${command}`);
|
||||
|
||||
if (state === 'greeting-sent') {
|
||||
if (command.startsWith('EHLO') || command.startsWith('HELO')) {
|
||||
socket.write('250 statemachine.example.com\r\n');
|
||||
state = 'ready';
|
||||
console.log(' [Server] State: greeting-sent -> ready');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
} else {
|
||||
socket.write('503 5.5.1 Bad sequence of commands\r\n');
|
||||
}
|
||||
} else if (state === 'ready') {
|
||||
if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
state = 'mail';
|
||||
console.log(' [Server] State: ready -> mail');
|
||||
} else if (command.startsWith('EHLO') || command.startsWith('HELO')) {
|
||||
socket.write('250 statemachine.example.com\r\n');
|
||||
// Stay in ready state
|
||||
} else if (command === 'RSET' || command === 'NOOP') {
|
||||
socket.write('250 OK\r\n');
|
||||
// Stay in ready state
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
} else {
|
||||
socket.write('503 5.5.1 Bad sequence of commands\r\n');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Just establish connection and send EHLO
|
||||
try {
|
||||
await smtpClient.verify();
|
||||
console.log(' Initial state transition (connect -> EHLO) successful');
|
||||
} catch (error) {
|
||||
console.log(` Connection/EHLO failed: ${error.message}`);
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 2: Transaction state machine
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing transaction state machine`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 statemachine.example.com ESMTP\r\n');
|
||||
|
||||
let state = 'ready';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] State: ${state}, Command: ${command}`);
|
||||
|
||||
switch (state) {
|
||||
case 'ready':
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250 statemachine.example.com\r\n');
|
||||
// Stay in ready
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
state = 'mail';
|
||||
console.log(' [Server] State: ready -> mail');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
} else {
|
||||
socket.write('503 5.5.1 Bad sequence of commands\r\n');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'mail':
|
||||
if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
state = 'rcpt';
|
||||
console.log(' [Server] State: mail -> rcpt');
|
||||
} else if (command === 'RSET') {
|
||||
socket.write('250 OK\r\n');
|
||||
state = 'ready';
|
||||
console.log(' [Server] State: mail -> ready (RSET)');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
} else {
|
||||
socket.write('503 5.5.1 Bad sequence of commands\r\n');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'rcpt':
|
||||
if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
// Stay in rcpt (can have multiple recipients)
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
state = 'data';
|
||||
console.log(' [Server] State: rcpt -> data');
|
||||
} else if (command === 'RSET') {
|
||||
socket.write('250 OK\r\n');
|
||||
state = 'ready';
|
||||
console.log(' [Server] State: rcpt -> ready (RSET)');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
} else {
|
||||
socket.write('503 5.5.1 Bad sequence of commands\r\n');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'data':
|
||||
if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
state = 'ready';
|
||||
console.log(' [Server] State: data -> ready (message complete)');
|
||||
} else if (command === 'QUIT') {
|
||||
// QUIT is not allowed during DATA
|
||||
socket.write('503 5.5.1 Bad sequence of commands\r\n');
|
||||
}
|
||||
// All other input during DATA is message content
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient1@example.com', 'recipient2@example.com'],
|
||||
subject: 'State machine test',
|
||||
text: 'Testing SMTP transaction state machine'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(' Complete transaction state sequence successful');
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 3: Invalid state transitions
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing invalid state transitions`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 statemachine.example.com ESMTP\r\n');
|
||||
|
||||
let state = 'ready';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] State: ${state}, Command: ${command}`);
|
||||
|
||||
// Strictly enforce state machine
|
||||
switch (state) {
|
||||
case 'ready':
|
||||
if (command.startsWith('EHLO') || command.startsWith('HELO')) {
|
||||
socket.write('250 statemachine.example.com\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
state = 'mail';
|
||||
} else if (command === 'RSET' || command === 'NOOP') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
console.log(' [Server] RCPT TO without MAIL FROM');
|
||||
socket.write('503 5.5.1 Need MAIL command first\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
console.log(' [Server] DATA without MAIL FROM and RCPT TO');
|
||||
socket.write('503 5.5.1 Need MAIL and RCPT commands first\r\n');
|
||||
} else {
|
||||
socket.write('503 5.5.1 Bad sequence of commands\r\n');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'mail':
|
||||
if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
state = 'rcpt';
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
console.log(' [Server] Second MAIL FROM without RSET');
|
||||
socket.write('503 5.5.1 Sender already specified\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
console.log(' [Server] DATA without RCPT TO');
|
||||
socket.write('503 5.5.1 Need RCPT command first\r\n');
|
||||
} else if (command === 'RSET') {
|
||||
socket.write('250 OK\r\n');
|
||||
state = 'ready';
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
} else {
|
||||
socket.write('503 5.5.1 Bad sequence of commands\r\n');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'rcpt':
|
||||
if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
state = 'data';
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
console.log(' [Server] MAIL FROM after RCPT TO without RSET');
|
||||
socket.write('503 5.5.1 Sender already specified\r\n');
|
||||
} else if (command === 'RSET') {
|
||||
socket.write('250 OK\r\n');
|
||||
state = 'ready';
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
} else {
|
||||
socket.write('503 5.5.1 Bad sequence of commands\r\n');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'data':
|
||||
if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
state = 'ready';
|
||||
} else if (command.startsWith('MAIL FROM:') ||
|
||||
command.startsWith('RCPT TO:') ||
|
||||
command === 'RSET') {
|
||||
console.log(' [Server] SMTP command during DATA mode');
|
||||
socket.write('503 5.5.1 Commands not allowed during data transfer\r\n');
|
||||
}
|
||||
// During DATA, most input is treated as message content
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// We'll create a custom client to send invalid command sequences
|
||||
const testCases = [
|
||||
{
|
||||
name: 'RCPT without MAIL',
|
||||
commands: ['EHLO client.example.com', 'RCPT TO:<test@example.com>'],
|
||||
expectError: true
|
||||
},
|
||||
{
|
||||
name: 'DATA without RCPT',
|
||||
commands: ['EHLO client.example.com', 'MAIL FROM:<sender@example.com>', 'DATA'],
|
||||
expectError: true
|
||||
},
|
||||
{
|
||||
name: 'Double MAIL FROM',
|
||||
commands: ['EHLO client.example.com', 'MAIL FROM:<sender1@example.com>', 'MAIL FROM:<sender2@example.com>'],
|
||||
expectError: true
|
||||
}
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
console.log(` Testing: ${testCase.name}`);
|
||||
|
||||
try {
|
||||
// Create simple socket connection for manual command testing
|
||||
const net = await import('net');
|
||||
const client = net.createConnection(testServer.port, testServer.hostname);
|
||||
|
||||
let responseCount = 0;
|
||||
let errorReceived = false;
|
||||
|
||||
client.on('data', (data) => {
|
||||
const response = data.toString();
|
||||
console.log(` Response: ${response.trim()}`);
|
||||
|
||||
if (response.startsWith('5')) {
|
||||
errorReceived = true;
|
||||
}
|
||||
|
||||
responseCount++;
|
||||
|
||||
if (responseCount <= testCase.commands.length) {
|
||||
const command = testCase.commands[responseCount - 1];
|
||||
if (command) {
|
||||
setTimeout(() => {
|
||||
console.log(` Sending: ${command}`);
|
||||
client.write(command + '\r\n');
|
||||
}, 100);
|
||||
}
|
||||
} else {
|
||||
client.write('QUIT\r\n');
|
||||
client.end();
|
||||
}
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
client.on('end', () => {
|
||||
if (testCase.expectError && errorReceived) {
|
||||
console.log(` ✓ Expected error received`);
|
||||
} else if (!testCase.expectError && !errorReceived) {
|
||||
console.log(` ✓ No error as expected`);
|
||||
} else {
|
||||
console.log(` ✗ Unexpected result`);
|
||||
}
|
||||
resolve(void 0);
|
||||
});
|
||||
|
||||
client.on('error', reject);
|
||||
|
||||
// Start with greeting response
|
||||
setTimeout(() => {
|
||||
if (testCase.commands.length > 0) {
|
||||
console.log(` Sending: ${testCase.commands[0]}`);
|
||||
client.write(testCase.commands[0] + '\r\n');
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.log(` Error testing ${testCase.name}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 4: RSET command state transitions
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing RSET command state transitions`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 statemachine.example.com ESMTP\r\n');
|
||||
|
||||
let state = 'ready';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] State: ${state}, Command: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250 statemachine.example.com\r\n');
|
||||
state = 'ready';
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
state = 'mail';
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
if (state === 'mail' || state === 'rcpt') {
|
||||
socket.write('250 OK\r\n');
|
||||
state = 'rcpt';
|
||||
} else {
|
||||
socket.write('503 5.5.1 Bad sequence of commands\r\n');
|
||||
}
|
||||
} else if (command === 'RSET') {
|
||||
console.log(` [Server] RSET from state: ${state} -> ready`);
|
||||
socket.write('250 OK\r\n');
|
||||
state = 'ready';
|
||||
} else if (command === 'DATA') {
|
||||
if (state === 'rcpt') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
state = 'data';
|
||||
} else {
|
||||
socket.write('503 5.5.1 Bad sequence of commands\r\n');
|
||||
}
|
||||
} else if (command === '.') {
|
||||
if (state === 'data') {
|
||||
socket.write('250 OK\r\n');
|
||||
state = 'ready';
|
||||
}
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
} else if (command === 'NOOP') {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Test RSET at various points in transaction
|
||||
console.log(' Testing RSET from different states...');
|
||||
|
||||
// We'll manually test RSET behavior
|
||||
const net = await import('net');
|
||||
const client = net.createConnection(testServer.port, testServer.hostname);
|
||||
|
||||
const commands = [
|
||||
'EHLO client.example.com', // -> ready
|
||||
'MAIL FROM:<sender@example.com>', // -> mail
|
||||
'RSET', // -> ready (reset from mail state)
|
||||
'MAIL FROM:<sender2@example.com>', // -> mail
|
||||
'RCPT TO:<rcpt1@example.com>', // -> rcpt
|
||||
'RCPT TO:<rcpt2@example.com>', // -> rcpt (multiple recipients)
|
||||
'RSET', // -> ready (reset from rcpt state)
|
||||
'MAIL FROM:<sender3@example.com>', // -> mail (fresh transaction)
|
||||
'RCPT TO:<rcpt3@example.com>', // -> rcpt
|
||||
'DATA', // -> data
|
||||
'.', // -> ready (complete transaction)
|
||||
'QUIT'
|
||||
];
|
||||
|
||||
let commandIndex = 0;
|
||||
|
||||
client.on('data', (data) => {
|
||||
const response = data.toString().trim();
|
||||
console.log(` Response: ${response}`);
|
||||
|
||||
if (commandIndex < commands.length) {
|
||||
setTimeout(() => {
|
||||
const command = commands[commandIndex];
|
||||
console.log(` Sending: ${command}`);
|
||||
if (command === 'DATA') {
|
||||
client.write(command + '\r\n');
|
||||
// Send message content immediately after DATA
|
||||
setTimeout(() => {
|
||||
client.write('Subject: RSET test\r\n\r\nTesting RSET state transitions.\r\n.\r\n');
|
||||
}, 100);
|
||||
} else {
|
||||
client.write(command + '\r\n');
|
||||
}
|
||||
commandIndex++;
|
||||
}, 100);
|
||||
} else {
|
||||
client.end();
|
||||
}
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
client.on('end', () => {
|
||||
console.log(' RSET state transitions completed successfully');
|
||||
resolve(void 0);
|
||||
});
|
||||
client.on('error', reject);
|
||||
});
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 5: Connection state persistence
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing connection state persistence`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 statemachine.example.com ESMTP\r\n');
|
||||
|
||||
let state = 'ready';
|
||||
let messageCount = 0;
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-statemachine.example.com\r\n');
|
||||
socket.write('250 PIPELINING\r\n');
|
||||
state = 'ready';
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
if (state === 'ready') {
|
||||
socket.write('250 OK\r\n');
|
||||
state = 'mail';
|
||||
} else {
|
||||
socket.write('503 5.5.1 Bad sequence\r\n');
|
||||
}
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
if (state === 'mail' || state === 'rcpt') {
|
||||
socket.write('250 OK\r\n');
|
||||
state = 'rcpt';
|
||||
} else {
|
||||
socket.write('503 5.5.1 Bad sequence\r\n');
|
||||
}
|
||||
} else if (command === 'DATA') {
|
||||
if (state === 'rcpt') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
state = 'data';
|
||||
} else {
|
||||
socket.write('503 5.5.1 Bad sequence\r\n');
|
||||
}
|
||||
} else if (command === '.') {
|
||||
if (state === 'data') {
|
||||
messageCount++;
|
||||
console.log(` [Server] Message ${messageCount} completed`);
|
||||
socket.write(`250 OK: Message ${messageCount} accepted\r\n`);
|
||||
state = 'ready';
|
||||
}
|
||||
} else if (command === 'QUIT') {
|
||||
console.log(` [Server] Session ended after ${messageCount} messages`);
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 1
|
||||
});
|
||||
|
||||
// Send multiple emails through same connection
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i}@example.com`],
|
||||
subject: `Persistence test ${i}`,
|
||||
text: `Testing connection state persistence - message ${i}`
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(` Message ${i} sent successfully`);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.response).toContain(`Message ${i}`);
|
||||
}
|
||||
|
||||
// Close the pooled connection
|
||||
await smtpClient.close();
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 6: Error state recovery
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing error state recovery`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 statemachine.example.com ESMTP\r\n');
|
||||
|
||||
let state = 'ready';
|
||||
let errorCount = 0;
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] State: ${state}, Command: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250 statemachine.example.com\r\n');
|
||||
state = 'ready';
|
||||
errorCount = 0; // Reset error count on new session
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
const address = command.match(/<(.+)>/)?.[1] || '';
|
||||
if (address.includes('error')) {
|
||||
errorCount++;
|
||||
console.log(` [Server] Error ${errorCount} - invalid sender`);
|
||||
socket.write('550 5.1.8 Invalid sender address\r\n');
|
||||
// State remains ready after error
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
state = 'mail';
|
||||
}
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
if (state === 'mail' || state === 'rcpt') {
|
||||
const address = command.match(/<(.+)>/)?.[1] || '';
|
||||
if (address.includes('error')) {
|
||||
errorCount++;
|
||||
console.log(` [Server] Error ${errorCount} - invalid recipient`);
|
||||
socket.write('550 5.1.1 User unknown\r\n');
|
||||
// State remains the same after recipient error
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
state = 'rcpt';
|
||||
}
|
||||
} else {
|
||||
socket.write('503 5.5.1 Bad sequence\r\n');
|
||||
}
|
||||
} else if (command === 'DATA') {
|
||||
if (state === 'rcpt') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
state = 'data';
|
||||
} else {
|
||||
socket.write('503 5.5.1 Bad sequence\r\n');
|
||||
}
|
||||
} else if (command === '.') {
|
||||
if (state === 'data') {
|
||||
socket.write('250 OK\r\n');
|
||||
state = 'ready';
|
||||
}
|
||||
} else if (command === 'RSET') {
|
||||
console.log(` [Server] RSET - recovering from errors (${errorCount} errors so far)`);
|
||||
socket.write('250 OK\r\n');
|
||||
state = 'ready';
|
||||
} else if (command === 'QUIT') {
|
||||
console.log(` [Server] Session ended with ${errorCount} total errors`);
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
} else {
|
||||
socket.write('500 5.5.1 Command not recognized\r\n');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Test recovery from various errors
|
||||
const testEmails = [
|
||||
{
|
||||
from: 'error@example.com', // Will cause sender error
|
||||
to: ['valid@example.com'],
|
||||
desc: 'invalid sender'
|
||||
},
|
||||
{
|
||||
from: 'valid@example.com',
|
||||
to: ['error@example.com', 'valid@example.com'], // Mixed valid/invalid recipients
|
||||
desc: 'mixed recipients'
|
||||
},
|
||||
{
|
||||
from: 'valid@example.com',
|
||||
to: ['valid@example.com'],
|
||||
desc: 'valid email after errors'
|
||||
}
|
||||
];
|
||||
|
||||
for (const testEmail of testEmails) {
|
||||
console.log(` Testing ${testEmail.desc}...`);
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: testEmail.from,
|
||||
to: testEmail.to,
|
||||
subject: `Error recovery test: ${testEmail.desc}`,
|
||||
text: `Testing error state recovery with ${testEmail.desc}`
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(` ${testEmail.desc}: Success`);
|
||||
if (result.rejected && result.rejected.length > 0) {
|
||||
console.log(` Rejected: ${result.rejected.length} recipients`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(` ${testEmail.desc}: Failed as expected - ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
console.log(`\n${testId}: All ${scenarioCount} state machine scenarios tested ✓`);
|
||||
});
|
@ -0,0 +1,686 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as plugins from './plugins.js';
|
||||
import { createTestServer } from '../../helpers/server.loader.js';
|
||||
import { createSmtpClient } from '../../helpers/smtp.client.js';
|
||||
|
||||
tap.test('CRFC-06: should handle protocol negotiation correctly (RFC 5321)', async (tools) => {
|
||||
const testId = 'CRFC-06-protocol-negotiation';
|
||||
console.log(`\n${testId}: Testing SMTP protocol negotiation compliance...`);
|
||||
|
||||
let scenarioCount = 0;
|
||||
|
||||
// Scenario 1: EHLO capability announcement and selection
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing EHLO capability announcement`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 negotiation.example.com ESMTP Service Ready\r\n');
|
||||
|
||||
let negotiatedCapabilities: string[] = [];
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
// Announce available capabilities
|
||||
socket.write('250-negotiation.example.com\r\n');
|
||||
socket.write('250-SIZE 52428800\r\n');
|
||||
socket.write('250-8BITMIME\r\n');
|
||||
socket.write('250-STARTTLS\r\n');
|
||||
socket.write('250-ENHANCEDSTATUSCODES\r\n');
|
||||
socket.write('250-PIPELINING\r\n');
|
||||
socket.write('250-CHUNKING\r\n');
|
||||
socket.write('250-SMTPUTF8\r\n');
|
||||
socket.write('250-DSN\r\n');
|
||||
socket.write('250-AUTH PLAIN LOGIN CRAM-MD5\r\n');
|
||||
socket.write('250 HELP\r\n');
|
||||
|
||||
negotiatedCapabilities = [
|
||||
'SIZE', '8BITMIME', 'STARTTLS', 'ENHANCEDSTATUSCODES',
|
||||
'PIPELINING', 'CHUNKING', 'SMTPUTF8', 'DSN', 'AUTH', 'HELP'
|
||||
];
|
||||
console.log(` [Server] Announced capabilities: ${negotiatedCapabilities.join(', ')}`);
|
||||
} else if (command.startsWith('HELO')) {
|
||||
// Basic SMTP mode - no capabilities
|
||||
socket.write('250 negotiation.example.com\r\n');
|
||||
negotiatedCapabilities = [];
|
||||
console.log(' [Server] Basic SMTP mode (no capabilities)');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
// Check for SIZE parameter
|
||||
const sizeMatch = command.match(/SIZE=(\d+)/i);
|
||||
if (sizeMatch && negotiatedCapabilities.includes('SIZE')) {
|
||||
const size = parseInt(sizeMatch[1]);
|
||||
console.log(` [Server] SIZE parameter used: ${size} bytes`);
|
||||
if (size > 52428800) {
|
||||
socket.write('552 5.3.4 Message size exceeds maximum\r\n');
|
||||
} else {
|
||||
socket.write('250 2.1.0 Sender OK\r\n');
|
||||
}
|
||||
} else if (sizeMatch && !negotiatedCapabilities.includes('SIZE')) {
|
||||
console.log(' [Server] SIZE parameter used without capability');
|
||||
socket.write('501 5.5.4 SIZE not supported\r\n');
|
||||
} else {
|
||||
socket.write('250 2.1.0 Sender OK\r\n');
|
||||
}
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
// Check for DSN parameters
|
||||
if (command.includes('NOTIFY=') && negotiatedCapabilities.includes('DSN')) {
|
||||
console.log(' [Server] DSN NOTIFY parameter used');
|
||||
} else if (command.includes('NOTIFY=') && !negotiatedCapabilities.includes('DSN')) {
|
||||
console.log(' [Server] DSN parameter used without capability');
|
||||
socket.write('501 5.5.4 DSN not supported\r\n');
|
||||
return;
|
||||
}
|
||||
socket.write('250 2.1.5 Recipient OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 2.0.0 Message accepted\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 2.0.0 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Test EHLO negotiation
|
||||
const esmtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Capability negotiation test',
|
||||
text: 'Testing EHLO capability announcement and usage'
|
||||
});
|
||||
|
||||
const result = await esmtpClient.sendMail(email);
|
||||
console.log(' EHLO capability negotiation successful');
|
||||
expect(result).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 2: Capability-based feature usage
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing capability-based feature usage`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 features.example.com ESMTP\r\n');
|
||||
|
||||
let supportsUTF8 = false;
|
||||
let supportsPipelining = false;
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-features.example.com\r\n');
|
||||
socket.write('250-SMTPUTF8\r\n');
|
||||
socket.write('250-PIPELINING\r\n');
|
||||
socket.write('250-8BITMIME\r\n');
|
||||
socket.write('250 SIZE 10485760\r\n');
|
||||
|
||||
supportsUTF8 = true;
|
||||
supportsPipelining = true;
|
||||
console.log(' [Server] UTF8 and PIPELINING capabilities announced');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
// Check for SMTPUTF8 parameter
|
||||
if (command.includes('SMTPUTF8') && supportsUTF8) {
|
||||
console.log(' [Server] SMTPUTF8 parameter accepted');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.includes('SMTPUTF8') && !supportsUTF8) {
|
||||
console.log(' [Server] SMTPUTF8 used without capability');
|
||||
socket.write('555 5.6.7 SMTPUTF8 not supported\r\n');
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Test with UTF-8 content
|
||||
const utf8Email = new plugins.smartmail.Email({
|
||||
from: 'sénder@example.com', // Non-ASCII sender
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'UTF-8 test: café, naïve, 你好',
|
||||
text: 'Testing SMTPUTF8 capability with international characters: émojis 🎉'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(utf8Email);
|
||||
console.log(' UTF-8 email sent using SMTPUTF8 capability');
|
||||
expect(result).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 3: Extension parameter validation
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing extension parameter validation`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 validation.example.com ESMTP\r\n');
|
||||
|
||||
const supportedExtensions = new Set(['SIZE', 'BODY', 'DSN', '8BITMIME']);
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-validation.example.com\r\n');
|
||||
socket.write('250-SIZE 5242880\r\n');
|
||||
socket.write('250-8BITMIME\r\n');
|
||||
socket.write('250-DSN\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
// Validate all ESMTP parameters
|
||||
const params = command.substring(command.indexOf('>') + 1).trim();
|
||||
if (params) {
|
||||
console.log(` [Server] Validating parameters: ${params}`);
|
||||
|
||||
const paramPairs = params.split(/\s+/).filter(p => p.length > 0);
|
||||
let allValid = true;
|
||||
|
||||
for (const param of paramPairs) {
|
||||
const [key, value] = param.split('=');
|
||||
|
||||
if (key === 'SIZE') {
|
||||
const size = parseInt(value || '0');
|
||||
if (isNaN(size) || size < 0) {
|
||||
socket.write('501 5.5.4 Invalid SIZE value\r\n');
|
||||
allValid = false;
|
||||
break;
|
||||
} else if (size > 5242880) {
|
||||
socket.write('552 5.3.4 Message size exceeds limit\r\n');
|
||||
allValid = false;
|
||||
break;
|
||||
}
|
||||
console.log(` [Server] SIZE=${size} validated`);
|
||||
} else if (key === 'BODY') {
|
||||
if (value !== '7BIT' && value !== '8BITMIME') {
|
||||
socket.write('501 5.5.4 Invalid BODY value\r\n');
|
||||
allValid = false;
|
||||
break;
|
||||
}
|
||||
console.log(` [Server] BODY=${value} validated`);
|
||||
} else if (key === 'RET') {
|
||||
if (value !== 'FULL' && value !== 'HDRS') {
|
||||
socket.write('501 5.5.4 Invalid RET value\r\n');
|
||||
allValid = false;
|
||||
break;
|
||||
}
|
||||
console.log(` [Server] RET=${value} validated`);
|
||||
} else if (key === 'ENVID') {
|
||||
// ENVID can be any string, just check format
|
||||
if (!value) {
|
||||
socket.write('501 5.5.4 ENVID requires value\r\n');
|
||||
allValid = false;
|
||||
break;
|
||||
}
|
||||
console.log(` [Server] ENVID=${value} validated`);
|
||||
} else {
|
||||
console.log(` [Server] Unknown parameter: ${key}`);
|
||||
socket.write(`555 5.5.4 Unsupported parameter: ${key}\r\n`);
|
||||
allValid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (allValid) {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
// Validate DSN parameters
|
||||
const params = command.substring(command.indexOf('>') + 1).trim();
|
||||
if (params) {
|
||||
const paramPairs = params.split(/\s+/).filter(p => p.length > 0);
|
||||
let allValid = true;
|
||||
|
||||
for (const param of paramPairs) {
|
||||
const [key, value] = param.split('=');
|
||||
|
||||
if (key === 'NOTIFY') {
|
||||
const notifyValues = value.split(',');
|
||||
const validNotify = ['NEVER', 'SUCCESS', 'FAILURE', 'DELAY'];
|
||||
|
||||
for (const nv of notifyValues) {
|
||||
if (!validNotify.includes(nv)) {
|
||||
socket.write('501 5.5.4 Invalid NOTIFY value\r\n');
|
||||
allValid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (allValid) {
|
||||
console.log(` [Server] NOTIFY=${value} validated`);
|
||||
}
|
||||
} else if (key === 'ORCPT') {
|
||||
// ORCPT format: addr-type;addr-value
|
||||
if (!value.includes(';')) {
|
||||
socket.write('501 5.5.4 Invalid ORCPT format\r\n');
|
||||
allValid = false;
|
||||
break;
|
||||
}
|
||||
console.log(` [Server] ORCPT=${value} validated`);
|
||||
} else {
|
||||
socket.write(`555 5.5.4 Unsupported RCPT parameter: ${key}\r\n`);
|
||||
allValid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (allValid) {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Test with various valid parameters
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Parameter validation test',
|
||||
text: 'Testing ESMTP parameter validation',
|
||||
dsn: {
|
||||
notify: ['SUCCESS', 'FAILURE'],
|
||||
envid: 'test-envelope-id-123',
|
||||
ret: 'FULL'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(' ESMTP parameter validation successful');
|
||||
expect(result).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 4: Service extension discovery
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing service extension discovery`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 discovery.example.com ESMTP Ready\r\n');
|
||||
|
||||
let clientName = '';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO ')) {
|
||||
clientName = command.substring(5);
|
||||
console.log(` [Server] Client identified as: ${clientName}`);
|
||||
|
||||
// Announce extensions in order of preference
|
||||
socket.write('250-discovery.example.com\r\n');
|
||||
|
||||
// Security extensions first
|
||||
socket.write('250-STARTTLS\r\n');
|
||||
socket.write('250-AUTH PLAIN LOGIN CRAM-MD5 DIGEST-MD5\r\n');
|
||||
|
||||
// Core functionality extensions
|
||||
socket.write('250-SIZE 104857600\r\n');
|
||||
socket.write('250-8BITMIME\r\n');
|
||||
socket.write('250-SMTPUTF8\r\n');
|
||||
|
||||
// Delivery extensions
|
||||
socket.write('250-DSN\r\n');
|
||||
socket.write('250-DELIVERBY 86400\r\n');
|
||||
|
||||
// Performance extensions
|
||||
socket.write('250-PIPELINING\r\n');
|
||||
socket.write('250-CHUNKING\r\n');
|
||||
socket.write('250-BINARYMIME\r\n');
|
||||
|
||||
// Enhanced status and debugging
|
||||
socket.write('250-ENHANCEDSTATUSCODES\r\n');
|
||||
socket.write('250-NO-SOLICITING\r\n');
|
||||
socket.write('250-MTRK\r\n');
|
||||
|
||||
// End with help
|
||||
socket.write('250 HELP\r\n');
|
||||
} else if (command.startsWith('HELO ')) {
|
||||
clientName = command.substring(5);
|
||||
console.log(` [Server] Basic SMTP client: ${clientName}`);
|
||||
socket.write('250 discovery.example.com\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
// Client should use discovered capabilities appropriately
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'HELP') {
|
||||
// Detailed help for discovered extensions
|
||||
socket.write('214-This server supports the following features:\r\n');
|
||||
socket.write('214-STARTTLS - Start TLS negotiation\r\n');
|
||||
socket.write('214-AUTH - SMTP Authentication\r\n');
|
||||
socket.write('214-SIZE - Message size declaration\r\n');
|
||||
socket.write('214-8BITMIME - 8-bit MIME transport\r\n');
|
||||
socket.write('214-SMTPUTF8 - UTF-8 support\r\n');
|
||||
socket.write('214-DSN - Delivery Status Notifications\r\n');
|
||||
socket.write('214-PIPELINING - Command pipelining\r\n');
|
||||
socket.write('214-CHUNKING - BDAT chunking\r\n');
|
||||
socket.write('214 For more information, visit our website\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Thank you for using our service\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
name: 'test-client.example.com'
|
||||
});
|
||||
|
||||
// Test service discovery
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Service discovery test',
|
||||
text: 'Testing SMTP service extension discovery'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(' Service extension discovery completed');
|
||||
expect(result).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 5: Backward compatibility negotiation
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing backward compatibility negotiation`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 compat.example.com ESMTP\r\n');
|
||||
|
||||
let isESMTP = false;
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
isESMTP = true;
|
||||
console.log(' [Server] ESMTP mode enabled');
|
||||
socket.write('250-compat.example.com\r\n');
|
||||
socket.write('250-SIZE 10485760\r\n');
|
||||
socket.write('250-8BITMIME\r\n');
|
||||
socket.write('250 ENHANCEDSTATUSCODES\r\n');
|
||||
} else if (command.startsWith('HELO')) {
|
||||
isESMTP = false;
|
||||
console.log(' [Server] Basic SMTP mode (RFC 821 compatibility)');
|
||||
socket.write('250 compat.example.com\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
if (isESMTP) {
|
||||
// Accept ESMTP parameters
|
||||
if (command.includes('SIZE=') || command.includes('BODY=')) {
|
||||
console.log(' [Server] ESMTP parameters accepted');
|
||||
}
|
||||
socket.write('250 2.1.0 Sender OK\r\n');
|
||||
} else {
|
||||
// Basic SMTP - reject ESMTP parameters
|
||||
if (command.includes('SIZE=') || command.includes('BODY=')) {
|
||||
console.log(' [Server] ESMTP parameters rejected in basic mode');
|
||||
socket.write('501 5.5.4 Syntax error in parameters\r\n');
|
||||
} else {
|
||||
socket.write('250 Sender OK\r\n');
|
||||
}
|
||||
}
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
if (isESMTP) {
|
||||
socket.write('250 2.1.5 Recipient OK\r\n');
|
||||
} else {
|
||||
socket.write('250 Recipient OK\r\n');
|
||||
}
|
||||
} else if (command === 'DATA') {
|
||||
if (isESMTP) {
|
||||
socket.write('354 2.0.0 Start mail input\r\n');
|
||||
} else {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
}
|
||||
} else if (command === '.') {
|
||||
if (isESMTP) {
|
||||
socket.write('250 2.0.0 Message accepted\r\n');
|
||||
} else {
|
||||
socket.write('250 Message accepted\r\n');
|
||||
}
|
||||
} else if (command === 'QUIT') {
|
||||
if (isESMTP) {
|
||||
socket.write('221 2.0.0 Service closing\r\n');
|
||||
} else {
|
||||
socket.write('221 Service closing\r\n');
|
||||
}
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Test ESMTP mode
|
||||
const esmtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
const esmtpEmail = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'ESMTP compatibility test',
|
||||
text: 'Testing ESMTP mode with extensions'
|
||||
});
|
||||
|
||||
const esmtpResult = await esmtpClient.sendMail(esmtpEmail);
|
||||
console.log(' ESMTP mode negotiation successful');
|
||||
expect(esmtpResult.response).toContain('2.0.0');
|
||||
|
||||
// Test basic SMTP mode (fallback)
|
||||
const basicClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
disableESMTP: true // Force HELO instead of EHLO
|
||||
});
|
||||
|
||||
const basicEmail = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Basic SMTP compatibility test',
|
||||
text: 'Testing basic SMTP mode without extensions'
|
||||
});
|
||||
|
||||
const basicResult = await basicClient.sendMail(basicEmail);
|
||||
console.log(' Basic SMTP mode fallback successful');
|
||||
expect(basicResult.response).not.toContain('2.0.0'); // No enhanced status codes
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 6: Extension interdependencies
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing extension interdependencies`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 interdep.example.com ESMTP\r\n');
|
||||
|
||||
let tlsEnabled = false;
|
||||
let authenticated = false;
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command} (TLS: ${tlsEnabled}, Auth: ${authenticated})`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-interdep.example.com\r\n');
|
||||
|
||||
if (!tlsEnabled) {
|
||||
// Before TLS
|
||||
socket.write('250-STARTTLS\r\n');
|
||||
socket.write('250-SIZE 1048576\r\n'); // Limited size before TLS
|
||||
} else {
|
||||
// After TLS
|
||||
socket.write('250-SIZE 52428800\r\n'); // Larger size after TLS
|
||||
socket.write('250-8BITMIME\r\n');
|
||||
socket.write('250-SMTPUTF8\r\n');
|
||||
socket.write('250-AUTH PLAIN LOGIN CRAM-MD5\r\n');
|
||||
|
||||
if (authenticated) {
|
||||
// Additional capabilities after authentication
|
||||
socket.write('250-DSN\r\n');
|
||||
socket.write('250-DELIVERBY 86400\r\n');
|
||||
}
|
||||
}
|
||||
|
||||
socket.write('250 ENHANCEDSTATUSCODES\r\n');
|
||||
} else if (command === 'STARTTLS') {
|
||||
if (!tlsEnabled) {
|
||||
socket.write('220 2.0.0 Ready to start TLS\r\n');
|
||||
tlsEnabled = true;
|
||||
console.log(' [Server] TLS enabled (simulated)');
|
||||
// In real implementation, would upgrade to TLS here
|
||||
} else {
|
||||
socket.write('503 5.5.1 TLS already active\r\n');
|
||||
}
|
||||
} else if (command.startsWith('AUTH')) {
|
||||
if (tlsEnabled) {
|
||||
authenticated = true;
|
||||
console.log(' [Server] Authentication successful (simulated)');
|
||||
socket.write('235 2.7.0 Authentication successful\r\n');
|
||||
} else {
|
||||
console.log(' [Server] AUTH rejected - TLS required');
|
||||
socket.write('538 5.7.11 Encryption required for authentication\r\n');
|
||||
}
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
if (command.includes('SMTPUTF8') && !tlsEnabled) {
|
||||
console.log(' [Server] SMTPUTF8 requires TLS');
|
||||
socket.write('530 5.7.0 Must issue STARTTLS first\r\n');
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
if (command.includes('NOTIFY=') && !authenticated) {
|
||||
console.log(' [Server] DSN requires authentication');
|
||||
socket.write('530 5.7.0 Authentication required for DSN\r\n');
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Test extension dependencies
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
requireTLS: true, // This will trigger STARTTLS
|
||||
auth: {
|
||||
user: 'testuser',
|
||||
pass: 'testpass'
|
||||
}
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Extension interdependency test',
|
||||
text: 'Testing SMTP extension interdependencies',
|
||||
dsn: {
|
||||
notify: ['SUCCESS'],
|
||||
envid: 'interdep-test-123'
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(' Extension interdependency handling successful');
|
||||
expect(result).toBeDefined();
|
||||
} catch (error) {
|
||||
console.log(` Extension dependency error (expected in test): ${error.message}`);
|
||||
// In test environment, STARTTLS won't actually work
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
console.log(`\n${testId}: All ${scenarioCount} protocol negotiation scenarios tested ✓`);
|
||||
});
|
@ -0,0 +1,726 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as plugins from './plugins.js';
|
||||
import { createTestServer } from '../../helpers/server.loader.js';
|
||||
import { createSmtpClient } from '../../helpers/smtp.client.js';
|
||||
|
||||
tap.test('CRFC-07: should ensure SMTP interoperability (RFC 5321)', async (tools) => {
|
||||
const testId = 'CRFC-07-interoperability';
|
||||
console.log(`\n${testId}: Testing SMTP interoperability compliance...`);
|
||||
|
||||
let scenarioCount = 0;
|
||||
|
||||
// Scenario 1: Different server implementations compatibility
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing different server implementations`);
|
||||
|
||||
const serverImplementations = [
|
||||
{
|
||||
name: 'Sendmail-style',
|
||||
greeting: '220 mail.example.com ESMTP Sendmail 8.15.2/8.15.2; Date Time',
|
||||
ehloResponse: [
|
||||
'250-mail.example.com Hello client.example.com [192.168.1.100]',
|
||||
'250-ENHANCEDSTATUSCODES',
|
||||
'250-PIPELINING',
|
||||
'250-8BITMIME',
|
||||
'250-SIZE 36700160',
|
||||
'250-DSN',
|
||||
'250-ETRN',
|
||||
'250-DELIVERBY',
|
||||
'250 HELP'
|
||||
],
|
||||
quirks: { verboseResponses: true, includesTimestamp: true }
|
||||
},
|
||||
{
|
||||
name: 'Postfix-style',
|
||||
greeting: '220 mail.example.com ESMTP Postfix',
|
||||
ehloResponse: [
|
||||
'250-mail.example.com',
|
||||
'250-PIPELINING',
|
||||
'250-SIZE 10240000',
|
||||
'250-VRFY',
|
||||
'250-ETRN',
|
||||
'250-STARTTLS',
|
||||
'250-ENHANCEDSTATUSCODES',
|
||||
'250-8BITMIME',
|
||||
'250-DSN',
|
||||
'250 SMTPUTF8'
|
||||
],
|
||||
quirks: { shortResponses: true, strictSyntax: true }
|
||||
},
|
||||
{
|
||||
name: 'Exchange-style',
|
||||
greeting: '220 mail.example.com Microsoft ESMTP MAIL Service ready',
|
||||
ehloResponse: [
|
||||
'250-mail.example.com Hello [192.168.1.100]',
|
||||
'250-SIZE 37748736',
|
||||
'250-PIPELINING',
|
||||
'250-DSN',
|
||||
'250-ENHANCEDSTATUSCODES',
|
||||
'250-STARTTLS',
|
||||
'250-8BITMIME',
|
||||
'250-BINARYMIME',
|
||||
'250-CHUNKING',
|
||||
'250 OK'
|
||||
],
|
||||
quirks: { windowsLineEndings: true, detailedErrors: true }
|
||||
}
|
||||
];
|
||||
|
||||
for (const impl of serverImplementations) {
|
||||
console.log(`\n Testing with ${impl.name} server...`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(` [${impl.name}] Client connected`);
|
||||
socket.write(impl.greeting + '\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [${impl.name}] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
impl.ehloResponse.forEach(line => {
|
||||
socket.write(line + '\r\n');
|
||||
});
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
if (impl.quirks.strictSyntax && !command.includes('<')) {
|
||||
socket.write('501 5.5.4 Syntax error in MAIL command\r\n');
|
||||
} else {
|
||||
const response = impl.quirks.verboseResponses ?
|
||||
'250 2.1.0 Sender OK' : '250 OK';
|
||||
socket.write(response + '\r\n');
|
||||
}
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
const response = impl.quirks.verboseResponses ?
|
||||
'250 2.1.5 Recipient OK' : '250 OK';
|
||||
socket.write(response + '\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
const response = impl.quirks.detailedErrors ?
|
||||
'354 Start mail input; end with <CRLF>.<CRLF>' :
|
||||
'354 Enter message, ending with "." on a line by itself';
|
||||
socket.write(response + '\r\n');
|
||||
} else if (command === '.') {
|
||||
const timestamp = impl.quirks.includesTimestamp ?
|
||||
` at ${new Date().toISOString()}` : '';
|
||||
socket.write(`250 2.0.0 Message accepted for delivery${timestamp}\r\n`);
|
||||
} else if (command === 'QUIT') {
|
||||
const response = impl.quirks.verboseResponses ?
|
||||
'221 2.0.0 Service closing transmission channel' :
|
||||
'221 Bye';
|
||||
socket.write(response + '\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: `Interoperability test with ${impl.name}`,
|
||||
text: `Testing compatibility with ${impl.name} server implementation`
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(` ${impl.name} compatibility: Success`);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
}
|
||||
})();
|
||||
|
||||
// Scenario 2: Character encoding and internationalization
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing character encoding interoperability`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 international.example.com ESMTP\r\n');
|
||||
|
||||
let supportsUTF8 = false;
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString();
|
||||
console.log(` [Server] Received (${data.length} bytes): ${command.trim()}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-international.example.com\r\n');
|
||||
socket.write('250-8BITMIME\r\n');
|
||||
socket.write('250-SMTPUTF8\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
supportsUTF8 = true;
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
// Check for non-ASCII characters
|
||||
const hasNonASCII = /[^\x00-\x7F]/.test(command);
|
||||
const hasUTF8Param = command.includes('SMTPUTF8');
|
||||
|
||||
console.log(` [Server] Non-ASCII: ${hasNonASCII}, UTF8 param: ${hasUTF8Param}`);
|
||||
|
||||
if (hasNonASCII && !hasUTF8Param) {
|
||||
socket.write('553 5.6.7 Non-ASCII addresses require SMTPUTF8\r\n');
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.trim() === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command.trim() === '.') {
|
||||
socket.write('250 OK: International message accepted\r\n');
|
||||
} else if (command.trim() === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Test various international character sets
|
||||
const internationalTests = [
|
||||
{
|
||||
desc: 'Latin characters with accents',
|
||||
from: 'sénder@éxample.com',
|
||||
to: 'récipient@éxample.com',
|
||||
subject: 'Tëst with açcénts',
|
||||
text: 'Café, naïve, résumé, piñata'
|
||||
},
|
||||
{
|
||||
desc: 'Cyrillic characters',
|
||||
from: 'отправитель@пример.com',
|
||||
to: 'получатель@пример.com',
|
||||
subject: 'Тест с кириллицей',
|
||||
text: 'Привет мир! Это тест с русскими буквами.'
|
||||
},
|
||||
{
|
||||
desc: 'Chinese characters',
|
||||
from: 'sender@example.com', // ASCII for compatibility
|
||||
to: 'recipient@example.com',
|
||||
subject: '测试中文字符',
|
||||
text: '你好世界!这是一个中文测试。'
|
||||
},
|
||||
{
|
||||
desc: 'Arabic characters',
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: 'اختبار النص العربي',
|
||||
text: 'مرحبا بالعالم! هذا اختبار باللغة العربية.'
|
||||
},
|
||||
{
|
||||
desc: 'Emoji and symbols',
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: '🎉 Test with emojis 🌟',
|
||||
text: 'Hello 👋 World 🌍! Testing emojis: 🚀 📧 ✨'
|
||||
}
|
||||
];
|
||||
|
||||
for (const test of internationalTests) {
|
||||
console.log(` Testing: ${test.desc}`);
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: test.from,
|
||||
to: [test.to],
|
||||
subject: test.subject,
|
||||
text: test.text
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(` ${test.desc}: Success`);
|
||||
expect(result).toBeDefined();
|
||||
} catch (error) {
|
||||
console.log(` ${test.desc}: Failed - ${error.message}`);
|
||||
// Some may fail if server doesn't support international addresses
|
||||
}
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 3: Message format compatibility
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing message format compatibility`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 formats.example.com ESMTP\r\n');
|
||||
|
||||
let inData = false;
|
||||
let messageContent = '';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
if (inData) {
|
||||
messageContent += data.toString();
|
||||
if (messageContent.includes('\r\n.\r\n')) {
|
||||
inData = false;
|
||||
|
||||
// Analyze message format
|
||||
const headers = messageContent.substring(0, messageContent.indexOf('\r\n\r\n'));
|
||||
const body = messageContent.substring(messageContent.indexOf('\r\n\r\n') + 4);
|
||||
|
||||
console.log(' [Server] Message analysis:');
|
||||
console.log(` Header count: ${(headers.match(/\r\n/g) || []).length + 1}`);
|
||||
console.log(` Body size: ${body.length} bytes`);
|
||||
|
||||
// Check for proper header folding
|
||||
const longHeaders = headers.split('\r\n').filter(h => h.length > 78);
|
||||
if (longHeaders.length > 0) {
|
||||
console.log(` Long headers detected: ${longHeaders.length}`);
|
||||
}
|
||||
|
||||
// Check for MIME structure
|
||||
if (headers.includes('Content-Type:')) {
|
||||
console.log(' MIME message detected');
|
||||
}
|
||||
|
||||
socket.write('250 OK: Message format validated\r\n');
|
||||
messageContent = '';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-formats.example.com\r\n');
|
||||
socket.write('250-8BITMIME\r\n');
|
||||
socket.write('250-BINARYMIME\r\n');
|
||||
socket.write('250 SIZE 52428800\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
inData = true;
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Test different message formats
|
||||
const formatTests = [
|
||||
{
|
||||
desc: 'Plain text message',
|
||||
email: new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Plain text test',
|
||||
text: 'This is a simple plain text message.'
|
||||
})
|
||||
},
|
||||
{
|
||||
desc: 'HTML message',
|
||||
email: new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'HTML test',
|
||||
html: '<h1>HTML Message</h1><p>This is an <strong>HTML</strong> message.</p>'
|
||||
})
|
||||
},
|
||||
{
|
||||
desc: 'Multipart alternative',
|
||||
email: new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Multipart test',
|
||||
text: 'Plain text version',
|
||||
html: '<p>HTML version</p>'
|
||||
})
|
||||
},
|
||||
{
|
||||
desc: 'Message with attachment',
|
||||
email: new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Attachment test',
|
||||
text: 'Message with attachment',
|
||||
attachments: [{
|
||||
filename: 'test.txt',
|
||||
content: 'This is a test attachment'
|
||||
}]
|
||||
})
|
||||
},
|
||||
{
|
||||
desc: 'Message with custom headers',
|
||||
email: new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Custom headers test',
|
||||
text: 'Message with custom headers',
|
||||
headers: {
|
||||
'X-Custom-Header': 'Custom value',
|
||||
'X-Mailer': 'Test Mailer 1.0',
|
||||
'Message-ID': '<test123@example.com>',
|
||||
'References': '<ref1@example.com> <ref2@example.com>'
|
||||
}
|
||||
})
|
||||
}
|
||||
];
|
||||
|
||||
for (const test of formatTests) {
|
||||
console.log(` Testing: ${test.desc}`);
|
||||
|
||||
const result = await smtpClient.sendMail(test.email);
|
||||
console.log(` ${test.desc}: Success`);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 4: Error handling interoperability
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing error handling interoperability`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 errors.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-errors.example.com\r\n');
|
||||
socket.write('250-ENHANCEDSTATUSCODES\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
const address = command.match(/<(.+)>/)?.[1] || '';
|
||||
|
||||
if (address.includes('temp-fail')) {
|
||||
// Temporary failure - client should retry
|
||||
socket.write('451 4.7.1 Temporary system problem, try again later\r\n');
|
||||
} else if (address.includes('perm-fail')) {
|
||||
// Permanent failure - client should not retry
|
||||
socket.write('550 5.1.8 Invalid sender address format\r\n');
|
||||
} else if (address.includes('syntax-error')) {
|
||||
// Syntax error
|
||||
socket.write('501 5.5.4 Syntax error in MAIL command\r\n');
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
const address = command.match(/<(.+)>/)?.[1] || '';
|
||||
|
||||
if (address.includes('unknown')) {
|
||||
socket.write('550 5.1.1 User unknown in local recipient table\r\n');
|
||||
} else if (address.includes('temp-reject')) {
|
||||
socket.write('450 4.2.1 Mailbox temporarily unavailable\r\n');
|
||||
} else if (address.includes('quota-exceeded')) {
|
||||
socket.write('552 5.2.2 Mailbox over quota\r\n');
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
} else {
|
||||
// Unknown command
|
||||
socket.write('500 5.5.1 Command unrecognized\r\n');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Test various error scenarios
|
||||
const errorTests = [
|
||||
{
|
||||
desc: 'Temporary sender failure',
|
||||
from: 'temp-fail@example.com',
|
||||
to: 'valid@example.com',
|
||||
expectError: true,
|
||||
errorType: '4xx'
|
||||
},
|
||||
{
|
||||
desc: 'Permanent sender failure',
|
||||
from: 'perm-fail@example.com',
|
||||
to: 'valid@example.com',
|
||||
expectError: true,
|
||||
errorType: '5xx'
|
||||
},
|
||||
{
|
||||
desc: 'Unknown recipient',
|
||||
from: 'valid@example.com',
|
||||
to: 'unknown@example.com',
|
||||
expectError: true,
|
||||
errorType: '5xx'
|
||||
},
|
||||
{
|
||||
desc: 'Mixed valid/invalid recipients',
|
||||
from: 'valid@example.com',
|
||||
to: ['valid@example.com', 'unknown@example.com', 'temp-reject@example.com'],
|
||||
expectError: false, // Partial success
|
||||
errorType: 'mixed'
|
||||
}
|
||||
];
|
||||
|
||||
for (const test of errorTests) {
|
||||
console.log(` Testing: ${test.desc}`);
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: test.from,
|
||||
to: Array.isArray(test.to) ? test.to : [test.to],
|
||||
subject: `Error test: ${test.desc}`,
|
||||
text: `Testing error handling for ${test.desc}`
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await smtpClient.sendMail(email);
|
||||
|
||||
if (test.expectError && test.errorType !== 'mixed') {
|
||||
console.log(` Unexpected success for ${test.desc}`);
|
||||
} else {
|
||||
console.log(` ${test.desc}: Handled correctly`);
|
||||
if (result.rejected && result.rejected.length > 0) {
|
||||
console.log(` Rejected: ${result.rejected.length} recipients`);
|
||||
}
|
||||
if (result.accepted && result.accepted.length > 0) {
|
||||
console.log(` Accepted: ${result.accepted.length} recipients`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (test.expectError) {
|
||||
console.log(` ${test.desc}: Failed as expected (${error.responseCode})`);
|
||||
if (test.errorType === '4xx') {
|
||||
expect(error.responseCode).toBeGreaterThanOrEqual(400);
|
||||
expect(error.responseCode).toBeLessThan(500);
|
||||
} else if (test.errorType === '5xx') {
|
||||
expect(error.responseCode).toBeGreaterThanOrEqual(500);
|
||||
expect(error.responseCode).toBeLessThan(600);
|
||||
}
|
||||
} else {
|
||||
console.log(` Unexpected error for ${test.desc}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 5: Connection management interoperability
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing connection management interoperability`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
|
||||
let commandCount = 0;
|
||||
let idleTime = Date.now();
|
||||
const maxIdleTime = 5000; // 5 seconds for testing
|
||||
const maxCommands = 10;
|
||||
|
||||
socket.write('220 connection.example.com ESMTP\r\n');
|
||||
|
||||
// Set up idle timeout
|
||||
const idleCheck = setInterval(() => {
|
||||
if (Date.now() - idleTime > maxIdleTime) {
|
||||
console.log(' [Server] Idle timeout - closing connection');
|
||||
socket.write('421 4.4.2 Idle timeout, closing connection\r\n');
|
||||
socket.end();
|
||||
clearInterval(idleCheck);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
commandCount++;
|
||||
idleTime = Date.now();
|
||||
|
||||
console.log(` [Server] Command ${commandCount}: ${command}`);
|
||||
|
||||
if (commandCount > maxCommands) {
|
||||
console.log(' [Server] Too many commands - closing connection');
|
||||
socket.write('421 4.7.0 Too many commands, closing connection\r\n');
|
||||
socket.end();
|
||||
clearInterval(idleCheck);
|
||||
return;
|
||||
}
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-connection.example.com\r\n');
|
||||
socket.write('250-PIPELINING\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'RSET') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'NOOP') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
clearInterval(idleCheck);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('close', () => {
|
||||
clearInterval(idleCheck);
|
||||
console.log(` [Server] Connection closed after ${commandCount} commands`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 1
|
||||
});
|
||||
|
||||
// Test connection reuse
|
||||
console.log(' Testing connection reuse...');
|
||||
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i}@example.com`],
|
||||
subject: `Connection test ${i}`,
|
||||
text: `Testing connection management - email ${i}`
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(` Email ${i} sent successfully`);
|
||||
expect(result).toBeDefined();
|
||||
|
||||
// Small delay to test connection persistence
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
// Test NOOP for keeping connection alive
|
||||
console.log(' Testing connection keep-alive...');
|
||||
|
||||
await smtpClient.verify(); // This might send NOOP
|
||||
console.log(' Connection verified (keep-alive)');
|
||||
|
||||
await smtpClient.close();
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 6: Legacy SMTP compatibility
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing legacy SMTP compatibility`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Legacy SMTP server');
|
||||
|
||||
// Old-style greeting without ESMTP
|
||||
socket.write('220 legacy.example.com Simple Mail Transfer Service Ready\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
// Legacy server doesn't understand EHLO
|
||||
socket.write('500 Command unrecognized\r\n');
|
||||
} else if (command.startsWith('HELO')) {
|
||||
socket.write('250 legacy.example.com\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
// Very strict syntax checking
|
||||
if (!command.match(/^MAIL FROM:\s*<[^>]+>\s*$/)) {
|
||||
socket.write('501 Syntax error\r\n');
|
||||
} else {
|
||||
socket.write('250 Sender OK\r\n');
|
||||
}
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
if (!command.match(/^RCPT TO:\s*<[^>]+>\s*$/)) {
|
||||
socket.write('501 Syntax error\r\n');
|
||||
} else {
|
||||
socket.write('250 Recipient OK\r\n');
|
||||
}
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Enter mail, end with "." on a line by itself\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 Message accepted for delivery\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Service closing transmission channel\r\n');
|
||||
socket.end();
|
||||
} else if (command === 'HELP') {
|
||||
socket.write('214-Commands supported:\r\n');
|
||||
socket.write('214-HELO MAIL RCPT DATA QUIT HELP\r\n');
|
||||
socket.write('214 End of HELP info\r\n');
|
||||
} else {
|
||||
socket.write('500 Command unrecognized\r\n');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Test with client that can fall back to basic SMTP
|
||||
const legacyClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
disableESMTP: true // Force HELO mode
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Legacy compatibility test',
|
||||
text: 'Testing compatibility with legacy SMTP servers'
|
||||
});
|
||||
|
||||
const result = await legacyClient.sendMail(email);
|
||||
console.log(' Legacy SMTP compatibility: Success');
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
console.log(`\n${testId}: All ${scenarioCount} interoperability scenarios tested ✓`);
|
||||
});
|
@ -0,0 +1,654 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as plugins from './plugins.js';
|
||||
import { createTestServer } from '../../helpers/server.loader.js';
|
||||
import { createSmtpClient } from '../../helpers/smtp.client.js';
|
||||
|
||||
tap.test('CRFC-08: should handle SMTP extensions correctly (Various RFCs)', async (tools) => {
|
||||
const testId = 'CRFC-08-smtp-extensions';
|
||||
console.log(`\n${testId}: Testing SMTP extensions compliance...`);
|
||||
|
||||
let scenarioCount = 0;
|
||||
|
||||
// Scenario 1: CHUNKING extension (RFC 3030)
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing CHUNKING extension (RFC 3030)`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 chunking.example.com ESMTP\r\n');
|
||||
|
||||
let chunkingMode = false;
|
||||
let totalChunks = 0;
|
||||
let totalBytes = 0;
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const text = data.toString();
|
||||
|
||||
if (chunkingMode) {
|
||||
// In chunking mode, all data is message content
|
||||
totalBytes += data.length;
|
||||
console.log(` [Server] Received chunk: ${data.length} bytes`);
|
||||
return;
|
||||
}
|
||||
|
||||
const command = text.trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-chunking.example.com\r\n');
|
||||
socket.write('250-CHUNKING\r\n');
|
||||
socket.write('250-8BITMIME\r\n');
|
||||
socket.write('250-BINARYMIME\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
if (command.includes('BODY=BINARYMIME')) {
|
||||
console.log(' [Server] Binary MIME body declared');
|
||||
}
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('BDAT ')) {
|
||||
// BDAT command format: BDAT <size> [LAST]
|
||||
const parts = command.split(' ');
|
||||
const chunkSize = parseInt(parts[1]);
|
||||
const isLast = parts.includes('LAST');
|
||||
|
||||
totalChunks++;
|
||||
console.log(` [Server] BDAT chunk ${totalChunks}: ${chunkSize} bytes${isLast ? ' (LAST)' : ''}`);
|
||||
|
||||
if (isLast) {
|
||||
socket.write(`250 OK: Message accepted, ${totalChunks} chunks, ${totalBytes} total bytes\r\n`);
|
||||
chunkingMode = false;
|
||||
totalChunks = 0;
|
||||
totalBytes = 0;
|
||||
} else {
|
||||
socket.write('250 OK: Chunk accepted\r\n');
|
||||
chunkingMode = true;
|
||||
}
|
||||
} else if (command === 'DATA') {
|
||||
// DATA not allowed when CHUNKING is available
|
||||
socket.write('503 5.5.1 Use BDAT instead of DATA\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Test with binary content that would benefit from chunking
|
||||
const binaryContent = Buffer.alloc(1024);
|
||||
for (let i = 0; i < binaryContent.length; i++) {
|
||||
binaryContent[i] = i % 256;
|
||||
}
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'CHUNKING test',
|
||||
text: 'Testing CHUNKING extension with binary data',
|
||||
attachments: [{
|
||||
filename: 'binary-data.bin',
|
||||
content: binaryContent
|
||||
}]
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(' CHUNKING extension handled (if supported by client)');
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 2: DELIVERBY extension (RFC 2852)
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing DELIVERBY extension (RFC 2852)`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 deliverby.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-deliverby.example.com\r\n');
|
||||
socket.write('250-DELIVERBY 86400\r\n'); // 24 hours max
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
// Check for DELIVERBY parameter
|
||||
const deliverByMatch = command.match(/DELIVERBY=(\d+)([RN]?)/i);
|
||||
if (deliverByMatch) {
|
||||
const seconds = parseInt(deliverByMatch[1]);
|
||||
const mode = deliverByMatch[2] || 'R'; // R=return, N=notify
|
||||
|
||||
console.log(` [Server] DELIVERBY: ${seconds} seconds, mode: ${mode}`);
|
||||
|
||||
if (seconds > 86400) {
|
||||
socket.write('501 5.5.4 DELIVERBY time exceeds maximum\r\n');
|
||||
} else if (seconds < 0) {
|
||||
socket.write('501 5.5.4 Invalid DELIVERBY time\r\n');
|
||||
} else {
|
||||
socket.write('250 OK: Delivery deadline accepted\r\n');
|
||||
}
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK: Message queued with delivery deadline\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Test with delivery deadline
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['urgent@example.com'],
|
||||
subject: 'Urgent delivery test',
|
||||
text: 'This message has a delivery deadline',
|
||||
// Note: Most SMTP clients don't expose DELIVERBY directly
|
||||
// but we can test server handling
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(' DELIVERBY extension supported by server');
|
||||
expect(result).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 3: ETRN extension (RFC 1985)
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing ETRN extension (RFC 1985)`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 etrn.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-etrn.example.com\r\n');
|
||||
socket.write('250-ETRN\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('ETRN ')) {
|
||||
const domain = command.substring(5);
|
||||
console.log(` [Server] ETRN request for domain: ${domain}`);
|
||||
|
||||
if (domain === '@example.com') {
|
||||
socket.write('250 OK: Queue processing started for example.com\r\n');
|
||||
} else if (domain === '#urgent') {
|
||||
socket.write('250 OK: Urgent queue processing started\r\n');
|
||||
} else if (domain.includes('unknown')) {
|
||||
socket.write('458 Unable to queue messages for node\r\n');
|
||||
} else {
|
||||
socket.write('250 OK: Queue processing started\r\n');
|
||||
}
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ETRN is typically used by mail servers, not clients
|
||||
// We'll test the server's ETRN capability manually
|
||||
const net = await import('net');
|
||||
const client = net.createConnection(testServer.port, testServer.hostname);
|
||||
|
||||
const commands = [
|
||||
'EHLO client.example.com',
|
||||
'ETRN @example.com', // Request queue processing for domain
|
||||
'ETRN #urgent', // Request urgent queue processing
|
||||
'ETRN unknown.domain.com', // Test error handling
|
||||
'QUIT'
|
||||
];
|
||||
|
||||
let commandIndex = 0;
|
||||
|
||||
client.on('data', (data) => {
|
||||
const response = data.toString().trim();
|
||||
console.log(` [Client] Response: ${response}`);
|
||||
|
||||
if (commandIndex < commands.length) {
|
||||
setTimeout(() => {
|
||||
const command = commands[commandIndex];
|
||||
console.log(` [Client] Sending: ${command}`);
|
||||
client.write(command + '\r\n');
|
||||
commandIndex++;
|
||||
}, 100);
|
||||
} else {
|
||||
client.end();
|
||||
}
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
client.on('end', () => {
|
||||
console.log(' ETRN extension testing completed');
|
||||
resolve(void 0);
|
||||
});
|
||||
client.on('error', reject);
|
||||
});
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 4: VRFY and EXPN extensions (RFC 5321)
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing VRFY and EXPN extensions`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 verify.example.com ESMTP\r\n');
|
||||
|
||||
// Simulated user database
|
||||
const users = new Map([
|
||||
['admin', { email: 'admin@example.com', fullName: 'Administrator' }],
|
||||
['john', { email: 'john.doe@example.com', fullName: 'John Doe' }],
|
||||
['support', { email: 'support@example.com', fullName: 'Support Team' }]
|
||||
]);
|
||||
|
||||
const mailingLists = new Map([
|
||||
['staff', ['admin@example.com', 'john.doe@example.com']],
|
||||
['support-team', ['support@example.com', 'admin@example.com']]
|
||||
]);
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-verify.example.com\r\n');
|
||||
socket.write('250-VRFY\r\n');
|
||||
socket.write('250-EXPN\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('VRFY ')) {
|
||||
const query = command.substring(5);
|
||||
console.log(` [Server] VRFY query: ${query}`);
|
||||
|
||||
// Look up user
|
||||
const user = users.get(query.toLowerCase());
|
||||
if (user) {
|
||||
socket.write(`250 ${user.fullName} <${user.email}>\r\n`);
|
||||
} else {
|
||||
// Check if it's an email address
|
||||
const emailMatch = Array.from(users.values()).find(u =>
|
||||
u.email.toLowerCase() === query.toLowerCase()
|
||||
);
|
||||
if (emailMatch) {
|
||||
socket.write(`250 ${emailMatch.fullName} <${emailMatch.email}>\r\n`);
|
||||
} else {
|
||||
socket.write('550 5.1.1 User unknown\r\n');
|
||||
}
|
||||
}
|
||||
} else if (command.startsWith('EXPN ')) {
|
||||
const listName = command.substring(5);
|
||||
console.log(` [Server] EXPN query: ${listName}`);
|
||||
|
||||
const list = mailingLists.get(listName.toLowerCase());
|
||||
if (list) {
|
||||
socket.write(`250-Mailing list ${listName}:\r\n`);
|
||||
list.forEach((email, index) => {
|
||||
const prefix = index < list.length - 1 ? '250-' : '250 ';
|
||||
socket.write(`${prefix}${email}\r\n`);
|
||||
});
|
||||
} else {
|
||||
socket.write('550 5.1.1 Mailing list not found\r\n');
|
||||
}
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Test VRFY and EXPN commands
|
||||
const net = await import('net');
|
||||
const client = net.createConnection(testServer.port, testServer.hostname);
|
||||
|
||||
const commands = [
|
||||
'EHLO client.example.com',
|
||||
'VRFY admin', // Verify user by username
|
||||
'VRFY john.doe@example.com', // Verify user by email
|
||||
'VRFY nonexistent', // Test unknown user
|
||||
'EXPN staff', // Expand mailing list
|
||||
'EXPN nonexistent-list', // Test unknown list
|
||||
'QUIT'
|
||||
];
|
||||
|
||||
let commandIndex = 0;
|
||||
|
||||
client.on('data', (data) => {
|
||||
const response = data.toString().trim();
|
||||
console.log(` [Client] Response: ${response}`);
|
||||
|
||||
if (commandIndex < commands.length) {
|
||||
setTimeout(() => {
|
||||
const command = commands[commandIndex];
|
||||
console.log(` [Client] Sending: ${command}`);
|
||||
client.write(command + '\r\n');
|
||||
commandIndex++;
|
||||
}, 200);
|
||||
} else {
|
||||
client.end();
|
||||
}
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
client.on('end', () => {
|
||||
console.log(' VRFY and EXPN testing completed');
|
||||
resolve(void 0);
|
||||
});
|
||||
client.on('error', reject);
|
||||
});
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 5: HELP extension
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing HELP extension`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 help.example.com ESMTP\r\n');
|
||||
|
||||
const helpTopics = new Map([
|
||||
['commands', [
|
||||
'Available commands:',
|
||||
'EHLO <domain> - Extended HELLO',
|
||||
'MAIL FROM:<addr> - Specify sender',
|
||||
'RCPT TO:<addr> - Specify recipient',
|
||||
'DATA - Start message text',
|
||||
'QUIT - Close connection'
|
||||
]],
|
||||
['extensions', [
|
||||
'Supported extensions:',
|
||||
'SIZE - Message size declaration',
|
||||
'8BITMIME - 8-bit MIME transport',
|
||||
'STARTTLS - Start TLS negotiation',
|
||||
'AUTH - SMTP Authentication',
|
||||
'DSN - Delivery Status Notifications'
|
||||
]],
|
||||
['syntax', [
|
||||
'Command syntax:',
|
||||
'Commands are case-insensitive',
|
||||
'Lines end with CRLF',
|
||||
'Email addresses must be in <> brackets',
|
||||
'Parameters are space-separated'
|
||||
]]
|
||||
]);
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-help.example.com\r\n');
|
||||
socket.write('250-HELP\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'HELP' || command === 'HELP HELP') {
|
||||
socket.write('214-This server provides HELP for the following topics:\r\n');
|
||||
socket.write('214-COMMANDS - List of available commands\r\n');
|
||||
socket.write('214-EXTENSIONS - List of supported extensions\r\n');
|
||||
socket.write('214-SYNTAX - Command syntax rules\r\n');
|
||||
socket.write('214 Use HELP <topic> for specific information\r\n');
|
||||
} else if (command.startsWith('HELP ')) {
|
||||
const topic = command.substring(5).toLowerCase();
|
||||
const helpText = helpTopics.get(topic);
|
||||
|
||||
if (helpText) {
|
||||
helpText.forEach((line, index) => {
|
||||
const prefix = index < helpText.length - 1 ? '214-' : '214 ';
|
||||
socket.write(`${prefix}${line}\r\n`);
|
||||
});
|
||||
} else {
|
||||
socket.write('504 5.3.0 HELP topic not available\r\n');
|
||||
}
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Test HELP command
|
||||
const net = await import('net');
|
||||
const client = net.createConnection(testServer.port, testServer.hostname);
|
||||
|
||||
const commands = [
|
||||
'EHLO client.example.com',
|
||||
'HELP', // General help
|
||||
'HELP COMMANDS', // Specific topic
|
||||
'HELP EXTENSIONS', // Another topic
|
||||
'HELP NONEXISTENT', // Unknown topic
|
||||
'QUIT'
|
||||
];
|
||||
|
||||
let commandIndex = 0;
|
||||
|
||||
client.on('data', (data) => {
|
||||
const response = data.toString().trim();
|
||||
console.log(` [Client] Response: ${response}`);
|
||||
|
||||
if (commandIndex < commands.length) {
|
||||
setTimeout(() => {
|
||||
const command = commands[commandIndex];
|
||||
console.log(` [Client] Sending: ${command}`);
|
||||
client.write(command + '\r\n');
|
||||
commandIndex++;
|
||||
}, 200);
|
||||
} else {
|
||||
client.end();
|
||||
}
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
client.on('end', () => {
|
||||
console.log(' HELP extension testing completed');
|
||||
resolve(void 0);
|
||||
});
|
||||
client.on('error', reject);
|
||||
});
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 6: Extension combination and interaction
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing extension combinations`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 combined.example.com ESMTP\r\n');
|
||||
|
||||
let activeExtensions: string[] = [];
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-combined.example.com\r\n');
|
||||
|
||||
// Announce multiple extensions
|
||||
const extensions = [
|
||||
'SIZE 52428800',
|
||||
'8BITMIME',
|
||||
'SMTPUTF8',
|
||||
'ENHANCEDSTATUSCODES',
|
||||
'PIPELINING',
|
||||
'DSN',
|
||||
'DELIVERBY 86400',
|
||||
'CHUNKING',
|
||||
'BINARYMIME',
|
||||
'HELP'
|
||||
];
|
||||
|
||||
extensions.forEach(ext => {
|
||||
socket.write(`250-${ext}\r\n`);
|
||||
activeExtensions.push(ext.split(' ')[0]);
|
||||
});
|
||||
|
||||
socket.write('250 OK\r\n');
|
||||
console.log(` [Server] Active extensions: ${activeExtensions.join(', ')}`);
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
// Check for multiple extension parameters
|
||||
const params = [];
|
||||
|
||||
if (command.includes('SIZE=')) {
|
||||
const sizeMatch = command.match(/SIZE=(\d+)/);
|
||||
if (sizeMatch) params.push(`SIZE=${sizeMatch[1]}`);
|
||||
}
|
||||
|
||||
if (command.includes('BODY=')) {
|
||||
const bodyMatch = command.match(/BODY=(\w+)/);
|
||||
if (bodyMatch) params.push(`BODY=${bodyMatch[1]}`);
|
||||
}
|
||||
|
||||
if (command.includes('SMTPUTF8')) {
|
||||
params.push('SMTPUTF8');
|
||||
}
|
||||
|
||||
if (command.includes('DELIVERBY=')) {
|
||||
const deliverByMatch = command.match(/DELIVERBY=(\d+)/);
|
||||
if (deliverByMatch) params.push(`DELIVERBY=${deliverByMatch[1]}`);
|
||||
}
|
||||
|
||||
if (params.length > 0) {
|
||||
console.log(` [Server] Extension parameters: ${params.join(', ')}`);
|
||||
}
|
||||
|
||||
socket.write('250 2.1.0 Sender OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
// Check for DSN parameters
|
||||
if (command.includes('NOTIFY=')) {
|
||||
const notifyMatch = command.match(/NOTIFY=([^,\s]+)/);
|
||||
if (notifyMatch) {
|
||||
console.log(` [Server] DSN NOTIFY: ${notifyMatch[1]}`);
|
||||
}
|
||||
}
|
||||
|
||||
socket.write('250 2.1.5 Recipient OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
if (activeExtensions.includes('CHUNKING')) {
|
||||
socket.write('503 5.5.1 Use BDAT when CHUNKING is available\r\n');
|
||||
} else {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
}
|
||||
} else if (command.startsWith('BDAT ')) {
|
||||
if (activeExtensions.includes('CHUNKING')) {
|
||||
const parts = command.split(' ');
|
||||
const size = parts[1];
|
||||
const isLast = parts.includes('LAST');
|
||||
console.log(` [Server] BDAT chunk: ${size} bytes${isLast ? ' (LAST)' : ''}`);
|
||||
|
||||
if (isLast) {
|
||||
socket.write('250 2.0.0 Message accepted\r\n');
|
||||
} else {
|
||||
socket.write('250 2.0.0 Chunk accepted\r\n');
|
||||
}
|
||||
} else {
|
||||
socket.write('500 5.5.1 CHUNKING not available\r\n');
|
||||
}
|
||||
} else if (command === '.') {
|
||||
socket.write('250 2.0.0 Message accepted\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 2.0.0 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Test email that could use multiple extensions
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Extension combination test with UTF-8: 测试',
|
||||
text: 'Testing multiple SMTP extensions together',
|
||||
dsn: {
|
||||
notify: ['SUCCESS', 'FAILURE'],
|
||||
envid: 'multi-ext-test-123'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(' Multiple extension combination handled');
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
console.log(`\n${testId}: All ${scenarioCount} SMTP extension scenarios tested ✓`);
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user