update
This commit is contained in:
@@ -0,0 +1,393 @@
|
||||
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('CEDGE-02: should handle malformed commands gracefully', async (tools) => {
|
||||
const testId = 'CEDGE-02-malformed-commands';
|
||||
console.log(`\n${testId}: Testing malformed command handling...`);
|
||||
|
||||
let scenarioCount = 0;
|
||||
|
||||
// Scenario 1: Commands with extra spaces
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing commands with extra spaces`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 mail.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
// Accept commands with extra spaces
|
||||
if (command.match(/^EHLO\s+/i)) {
|
||||
socket.write('250-mail.example.com\r\n');
|
||||
socket.write('250 STARTTLS\r\n');
|
||||
} else if (command.match(/^MAIL\s+FROM:/i)) {
|
||||
// Even with multiple spaces
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.match(/^RCPT\s+TO:/i)) {
|
||||
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 sending with commands that might have extra spaces
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Test with extra spaces',
|
||||
text: 'Testing command formatting'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(` Result: ${result.messageId ? 'Success' : 'Failed'}`);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 2: Commands with incorrect case mixing
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing mixed case commands`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 mail.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
// RFC says commands should be case-insensitive
|
||||
const upperCommand = command.toUpperCase();
|
||||
|
||||
if (upperCommand.startsWith('EHLO')) {
|
||||
socket.write('250-mail.example.com\r\n');
|
||||
socket.write('250 8BITMIME\r\n');
|
||||
} else if (upperCommand.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (upperCommand.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (upperCommand === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (upperCommand === 'QUIT') {
|
||||
socket.write('221 Bye\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: 'Mixed case test',
|
||||
text: 'Testing case sensitivity'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(` Result: ${result.messageId ? 'Success' : 'Failed'}`);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 3: Commands with missing parameters
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing commands with missing parameters`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 mail.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
if (command === 'EHLO' || command === 'EHLO ') {
|
||||
// Missing hostname
|
||||
socket.write('501 Syntax error in parameters or arguments\r\n');
|
||||
} else {
|
||||
socket.write('250-mail.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else if (command === 'MAIL FROM:' || command === 'MAIL') {
|
||||
// Missing address
|
||||
socket.write('501 Syntax error in parameters or arguments\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'RCPT TO:' || command === 'RCPT') {
|
||||
// Missing address
|
||||
socket.write('501 Syntax error in parameters or arguments\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 with a forgiving client that handles syntax errors
|
||||
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: 'Syntax error recovery test',
|
||||
text: 'Testing error recovery'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(` Result: ${result.messageId ? 'Success' : 'Failed'}`);
|
||||
expect(result).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 4: Commands with invalid syntax
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing invalid command syntax`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 mail.example.com ESMTP\r\n');
|
||||
|
||||
let state = 'connected';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-mail.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
state = 'ready';
|
||||
} else if (command.includes('FROM') && !command.startsWith('MAIL FROM:')) {
|
||||
// Invalid MAIL command format
|
||||
socket.write('500 Syntax error, command unrecognized\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
state = 'mail';
|
||||
} else if (command.includes('TO') && !command.startsWith('RCPT TO:')) {
|
||||
// Invalid RCPT command format
|
||||
socket.write('500 Syntax error, command unrecognized\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
state = 'rcpt';
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
state = 'data';
|
||||
} else if (command === '.' && state === 'data') {
|
||||
socket.write('250 OK\r\n');
|
||||
state = 'done';
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
} else if (state !== 'data') {
|
||||
// Unknown command
|
||||
socket.write('502 Command not implemented\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: 'Invalid syntax test',
|
||||
text: 'Testing invalid command handling'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(` Result: ${result.messageId ? 'Success' : 'Failed'}`);
|
||||
expect(result).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 5: Commands sent out of order
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing out-of-order commands`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 mail.example.com ESMTP\r\n');
|
||||
|
||||
let state = 'connected';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command} (state: ${state})`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-mail.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
state = 'ready';
|
||||
} else if (command.startsWith('RCPT TO:') && state !== 'mail' && state !== 'rcpt') {
|
||||
// RCPT before MAIL
|
||||
socket.write('503 Bad sequence of commands\r\n');
|
||||
} else if (command.startsWith('DATA') && state !== 'rcpt') {
|
||||
// DATA before RCPT
|
||||
socket.write('503 Bad sequence of commands\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
if (state === 'ready' || state === 'done') {
|
||||
socket.write('250 OK\r\n');
|
||||
state = 'mail';
|
||||
} else {
|
||||
socket.write('503 Bad sequence of commands\r\n');
|
||||
}
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
state = 'rcpt';
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
state = 'data';
|
||||
} else if (command === '.' && state === 'data') {
|
||||
socket.write('250 OK\r\n');
|
||||
state = 'done';
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\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: 'Command sequence test',
|
||||
text: 'Testing command ordering'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(` Result: ${result.messageId ? 'Success' : 'Failed'}`);
|
||||
expect(result).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 6: Commands with invalid characters
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing commands with invalid characters`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 mail.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString();
|
||||
const printable = command.replace(/[\r\n]/g, '\\r\\n').replace(/[^\x20-\x7E]/g, '?');
|
||||
console.log(` [Server] Received: ${printable}`);
|
||||
|
||||
// Check for non-ASCII characters
|
||||
if (/[^\x00-\x7F]/.test(command)) {
|
||||
socket.write('500 Syntax error, non-ASCII characters not allowed\r\n');
|
||||
return;
|
||||
}
|
||||
|
||||
const cleanCommand = command.trim();
|
||||
|
||||
if (cleanCommand.startsWith('EHLO')) {
|
||||
socket.write('250-mail.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (cleanCommand.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (cleanCommand.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (cleanCommand === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (cleanCommand === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (cleanCommand === 'QUIT') {
|
||||
socket.write('221 Bye\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: 'Character encoding test',
|
||||
text: 'Testing character validation'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(` Result: ${result.messageId ? 'Success' : 'Failed'}`);
|
||||
expect(result).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
console.log(`\n${testId}: All ${scenarioCount} malformed command scenarios tested ✓`);
|
||||
});
|
@@ -0,0 +1,384 @@
|
||||
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('CEDGE-03: should handle protocol violations gracefully', async (tools) => {
|
||||
const testId = 'CEDGE-03-protocol-violations';
|
||||
console.log(`\n${testId}: Testing protocol violation handling...`);
|
||||
|
||||
let scenarioCount = 0;
|
||||
|
||||
// Scenario 1: Server closes connection unexpectedly
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing unexpected connection closure`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 mail.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-mail.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
// Abruptly close connection
|
||||
console.log(' [Server] Closing connection unexpectedly');
|
||||
socket.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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: 'Connection closure test',
|
||||
text: 'Testing unexpected disconnection'
|
||||
});
|
||||
|
||||
try {
|
||||
await smtpClient.sendMail(email);
|
||||
console.log(' Unexpected success');
|
||||
} catch (error) {
|
||||
console.log(` Expected error: ${error.message}`);
|
||||
expect(error).toBeDefined();
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 2: Server sends data without CRLF
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing responses without proper CRLF`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
// Send greeting without CRLF
|
||||
socket.write('220 mail.example.com ESMTP');
|
||||
// Then send proper CRLF
|
||||
setTimeout(() => socket.write('\r\n'), 100);
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
// Mix responses with and without CRLF
|
||||
socket.write('250-mail.example.com\n'); // Just LF
|
||||
socket.write('250-SIZE 10485760\r'); // Just CR
|
||||
socket.write('250 OK\r\n'); // Proper CRLF
|
||||
} 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
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Line ending test',
|
||||
text: 'Testing non-standard line endings'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(` Result: ${result.messageId ? 'Success (handled gracefully)' : 'Failed'}`);
|
||||
expect(result).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 3: Server sends responses in wrong order
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing out-of-order responses`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 mail.example.com ESMTP\r\n');
|
||||
|
||||
const pendingResponses: Array<() => void> = [];
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-mail.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
// Delay response
|
||||
pendingResponses.push(() => {
|
||||
socket.write('250 OK\r\n');
|
||||
});
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
// Send this response first, then the MAIL response
|
||||
socket.write('250 OK\r\n');
|
||||
if (pendingResponses.length > 0) {
|
||||
pendingResponses.forEach(fn => fn());
|
||||
pendingResponses.length = 0;
|
||||
}
|
||||
} 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
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Response order test',
|
||||
text: 'Testing out-of-order responses'
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(` Result: ${result.messageId ? 'Success' : 'Failed'}`);
|
||||
} catch (error) {
|
||||
console.log(` Expected possible error: ${error.message}`);
|
||||
expect(error).toBeDefined();
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 4: Server sends unsolicited responses
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing unsolicited server responses`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 mail.example.com ESMTP\r\n');
|
||||
|
||||
// Send unsolicited responses periodically
|
||||
const interval = setInterval(() => {
|
||||
if (!socket.destroyed) {
|
||||
console.log(' [Server] Sending unsolicited response');
|
||||
socket.write('250-NOTICE: Server status update\r\n');
|
||||
}
|
||||
}, 500);
|
||||
|
||||
socket.on('close', () => clearInterval(interval));
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-mail.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') {
|
||||
clearInterval(interval);
|
||||
socket.write('221 Bye\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: 'Unsolicited response test',
|
||||
text: 'Testing unsolicited server messages'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(` Result: ${result.messageId ? 'Success' : 'Failed'}`);
|
||||
expect(result).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 5: Server violates response code format
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing invalid response codes`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
// Invalid response code (should be 3 digits)
|
||||
socket.write('22 mail.example.com ESMTP\r\n');
|
||||
setTimeout(() => {
|
||||
// Send correct response
|
||||
socket.write('220 mail.example.com ESMTP\r\n');
|
||||
}, 100);
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
// Mix valid and invalid response codes
|
||||
socket.write('250-mail.example.com\r\n');
|
||||
socket.write('25O-TYPO IN CODE\r\n'); // Letter O instead of zero
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('2.5.0 OK\r\n'); // Wrong format
|
||||
setTimeout(() => {
|
||||
socket.write('250 OK\r\n');
|
||||
}, 50);
|
||||
} 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
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Response code test',
|
||||
text: 'Testing invalid response codes'
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(` Result: ${result.messageId ? 'Success (handled invalid codes)' : 'Failed'}`);
|
||||
} catch (error) {
|
||||
console.log(` Expected possible error: ${error.message}`);
|
||||
expect(error).toBeDefined();
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 6: Server sends binary data
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing binary data in responses`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
// Send greeting with some binary data
|
||||
socket.write('220 mail.example.com ESMTP\r\n');
|
||||
socket.write(Buffer.from([0x00, 0x01, 0x02, 0x03])); // Binary data
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-mail.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 === '.') {
|
||||
// Include binary in response
|
||||
socket.write('250 OK ');
|
||||
socket.write(Buffer.from([0xFF, 0xFE]));
|
||||
socket.write('\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 email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Binary data test',
|
||||
text: 'Testing binary data handling'
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(` Result: ${result.messageId ? 'Success' : 'Failed'}`);
|
||||
expect(result).toBeDefined();
|
||||
} catch (error) {
|
||||
console.log(` Error handling binary data: ${error.message}`);
|
||||
expect(error).toBeDefined();
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
console.log(`\n${testId}: All ${scenarioCount} protocol violation scenarios tested ✓`);
|
||||
});
|
@@ -0,0 +1,488 @@
|
||||
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('CEDGE-04: should handle resource constraints gracefully', async (tools) => {
|
||||
const testId = 'CEDGE-04-resource-constraints';
|
||||
console.log(`\n${testId}: Testing resource constraint handling...`);
|
||||
|
||||
let scenarioCount = 0;
|
||||
|
||||
// Scenario 1: Very slow server responses
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing very slow server responses`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
// Slow greeting
|
||||
setTimeout(() => {
|
||||
socket.write('220 mail.example.com ESMTP\r\n');
|
||||
}, 2000);
|
||||
|
||||
socket.on('data', async (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
// Add delays to all responses
|
||||
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
await delay(1500);
|
||||
socket.write('250-mail.example.com\r\n');
|
||||
await delay(500);
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
await delay(2000);
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
await delay(1000);
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
await delay(1500);
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
await delay(3000);
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
await delay(500);
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 10000, // 10 second timeout
|
||||
greetingTimeout: 5000,
|
||||
socketTimeout: 10000
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Slow server test',
|
||||
text: 'Testing slow server responses'
|
||||
});
|
||||
|
||||
console.log(' Sending email (this will take time due to delays)...');
|
||||
const start = Date.now();
|
||||
const result = await smtpClient.sendMail(email);
|
||||
const elapsed = Date.now() - start;
|
||||
console.log(` Result: ${result.messageId ? 'Success' : 'Failed'} (took ${elapsed}ms)`);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 2: Server with limited buffer (sends data in small chunks)
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing server sending data in small chunks`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
|
||||
// Send greeting in small chunks
|
||||
const greeting = '220 mail.example.com ESMTP\r\n';
|
||||
for (let i = 0; i < greeting.length; i += 5) {
|
||||
socket.write(greeting.slice(i, i + 5));
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
}
|
||||
|
||||
socket.on('data', async (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
// Send capabilities in very small chunks
|
||||
const response = '250-mail.example.com\r\n250-SIZE 10485760\r\n250-8BITMIME\r\n250 OK\r\n';
|
||||
for (let i = 0; i < response.length; i += 3) {
|
||||
socket.write(response.slice(i, i + 3));
|
||||
await new Promise(resolve => setTimeout(resolve, 20));
|
||||
}
|
||||
} 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
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Chunked response test',
|
||||
text: 'Testing fragmented server responses'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(` Result: ${result.messageId ? 'Success' : 'Failed'}`);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 3: Server with connection limit
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing server connection limits`);
|
||||
|
||||
let connectionCount = 0;
|
||||
const maxConnections = 2;
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
connectionCount++;
|
||||
console.log(` [Server] Connection ${connectionCount} (max: ${maxConnections})`);
|
||||
|
||||
if (connectionCount > maxConnections) {
|
||||
socket.write('421 4.3.2 Too many connections, try again later\r\n');
|
||||
socket.end();
|
||||
return;
|
||||
}
|
||||
|
||||
socket.write('220 mail.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('close', () => {
|
||||
connectionCount--;
|
||||
console.log(` [Server] Connection closed, count: ${connectionCount}`);
|
||||
});
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-mail.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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Try to send multiple emails concurrently
|
||||
const emails = Array(3).fill(null).map((_, i) =>
|
||||
new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i + 1}@example.com`],
|
||||
subject: `Connection limit test ${i + 1}`,
|
||||
text: `Testing connection limits - email ${i + 1}`
|
||||
})
|
||||
);
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
emails.map(async (email, i) => {
|
||||
const client = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
console.log(` Sending email ${i + 1}...`);
|
||||
try {
|
||||
const result = await client.sendMail(email);
|
||||
return { index: i + 1, success: true, result };
|
||||
} catch (error) {
|
||||
return { index: i + 1, success: false, error: error.message };
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
results.forEach((result, i) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
const { index, success, error } = result.value;
|
||||
console.log(` Email ${index}: ${success ? 'Success' : `Failed - ${error}`}`);
|
||||
}
|
||||
});
|
||||
|
||||
// At least some should succeed
|
||||
const successes = results.filter(r =>
|
||||
r.status === 'fulfilled' && r.value.success
|
||||
);
|
||||
expect(successes.length).toBeGreaterThan(0);
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 4: Server with memory constraints (limited line length)
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing server line length limits`);
|
||||
|
||||
const maxLineLength = 100;
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 mail.example.com ESMTP\r\n');
|
||||
|
||||
let buffer = '';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
buffer += data.toString();
|
||||
|
||||
let lines = buffer.split('\r\n');
|
||||
buffer = lines.pop() || ''; // Keep incomplete line in buffer
|
||||
|
||||
lines.forEach(line => {
|
||||
if (line.length === 0) return;
|
||||
|
||||
console.log(` [Server] Received line (${line.length} chars): ${line.substring(0, 50)}...`);
|
||||
|
||||
if (line.length > maxLineLength && !line.startsWith('DATA')) {
|
||||
socket.write(`500 5.5.2 Line too long (max ${maxLineLength})\r\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (line.startsWith('EHLO')) {
|
||||
socket.write('250-mail.example.com\r\n');
|
||||
socket.write(`250-SIZE ${maxLineLength}\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();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Test with normal email first
|
||||
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: 'Line length test',
|
||||
text: 'Testing server line length limits with a reasonably short message that should work fine.'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(` Normal email result: ${result.messageId ? 'Success' : 'Failed'}`);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
|
||||
// Test with very long subject
|
||||
const longEmail = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'A'.repeat(150), // Very long subject
|
||||
text: 'Short body'
|
||||
});
|
||||
|
||||
try {
|
||||
const longResult = await smtpClient.sendMail(longEmail);
|
||||
console.log(` Long subject email: ${longResult.messageId ? 'Success (folded properly)' : 'Failed'}`);
|
||||
} catch (error) {
|
||||
console.log(` Long subject email failed as expected: ${error.message}`);
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 5: Server with CPU constraints (slow command processing)
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing server with slow command processing`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 mail.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', async (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
// Simulate CPU-intensive processing
|
||||
const busyWait = (ms: number) => {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < ms) {
|
||||
// Busy wait
|
||||
}
|
||||
};
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
busyWait(500);
|
||||
socket.write('250-mail.example.com\r\n');
|
||||
busyWait(200);
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
busyWait(300);
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
busyWait(400);
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
busyWait(200);
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
busyWait(1000); // Slow processing of message
|
||||
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,
|
||||
socketTimeout: 10000 // Higher timeout for slow server
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'CPU constraint test',
|
||||
text: 'Testing server with slow processing'
|
||||
});
|
||||
|
||||
console.log(' Sending email to slow server...');
|
||||
const start = Date.now();
|
||||
const result = await smtpClient.sendMail(email);
|
||||
const elapsed = Date.now() - start;
|
||||
console.log(` Result: ${result.messageId ? 'Success' : 'Failed'} (took ${elapsed}ms)`);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
expect(elapsed).toBeGreaterThan(2000); // Should take at least 2 seconds
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 6: Server with limited command buffer
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing server with limited command buffer`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 mail.example.com ESMTP\r\n');
|
||||
|
||||
const commandQueue: string[] = [];
|
||||
let processing = false;
|
||||
|
||||
const processCommands = async () => {
|
||||
if (processing || commandQueue.length === 0) return;
|
||||
processing = true;
|
||||
|
||||
while (commandQueue.length > 0) {
|
||||
const command = commandQueue.shift()!;
|
||||
console.log(` [Server] Processing: ${command}`);
|
||||
|
||||
// Simulate slow processing
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-mail.example.com\r\n');
|
||||
socket.write('250-PIPELINING\r\n'); // Support pipelining
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
processing = false;
|
||||
};
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const commands = data.toString().split('\r\n').filter(cmd => cmd.length > 0);
|
||||
|
||||
commands.forEach(cmd => {
|
||||
if (commandQueue.length >= 5) {
|
||||
console.log(' [Server] Command buffer full, rejecting command');
|
||||
socket.write('421 4.3.2 Command buffer full\r\n');
|
||||
socket.end();
|
||||
return;
|
||||
}
|
||||
commandQueue.push(cmd);
|
||||
});
|
||||
|
||||
processCommands();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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: 'Command buffer test',
|
||||
text: 'Testing limited command buffer'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(` Result: ${result.messageId ? 'Success' : 'Failed'}`);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
console.log(`\n${testId}: All ${scenarioCount} resource constraint scenarios tested ✓`);
|
||||
});
|
@@ -0,0 +1,535 @@
|
||||
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('CEDGE-05: should handle encoding issues gracefully', async (tools) => {
|
||||
const testId = 'CEDGE-05-encoding-issues';
|
||||
console.log(`\n${testId}: Testing encoding issue handling...`);
|
||||
|
||||
let scenarioCount = 0;
|
||||
|
||||
// Scenario 1: Mixed character encodings in email content
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing mixed character encodings`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 mail.example.com ESMTP\r\n');
|
||||
|
||||
let inData = false;
|
||||
let messageData = '';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const text = data.toString();
|
||||
|
||||
if (inData) {
|
||||
messageData += text;
|
||||
if (text.includes('\r\n.\r\n')) {
|
||||
inData = false;
|
||||
console.log(` [Server] Received message data (${messageData.length} bytes)`);
|
||||
|
||||
// Check for various encodings
|
||||
const hasUtf8 = /[\u0080-\uFFFF]/.test(messageData);
|
||||
const hasBase64 = /Content-Transfer-Encoding:\s*base64/i.test(messageData);
|
||||
const hasQuotedPrintable = /Content-Transfer-Encoding:\s*quoted-printable/i.test(messageData);
|
||||
|
||||
console.log(` [Server] Encodings detected: UTF-8=${hasUtf8}, Base64=${hasBase64}, QP=${hasQuotedPrintable}`);
|
||||
|
||||
socket.write('250 OK\r\n');
|
||||
messageData = '';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const command = text.trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-mail.example.com\r\n');
|
||||
socket.write('250-8BITMIME\r\n');
|
||||
socket.write('250-SMTPUTF8\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
|
||||
});
|
||||
|
||||
// Email with mixed encodings
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Test with émojis 🎉 and spéçiål characters',
|
||||
text: 'Plain text with Unicode: café, naïve, 你好, مرحبا',
|
||||
html: '<p>HTML with entities: café, naïve, and emoji 🌟</p>',
|
||||
attachments: [{
|
||||
filename: 'tëst-filé.txt',
|
||||
content: 'Attachment content with special chars: ñ, ü, ß'
|
||||
}]
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(` Result: ${result.messageId ? 'Success' : 'Failed'}`);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 2: Invalid UTF-8 sequences
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing invalid UTF-8 sequences`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 mail.example.com ESMTP\r\n');
|
||||
|
||||
let inData = false;
|
||||
|
||||
socket.on('data', (data) => {
|
||||
if (inData) {
|
||||
if (data.toString().includes('\r\n.\r\n')) {
|
||||
inData = false;
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-mail.example.com\r\n');
|
||||
socket.write('250-8BITMIME\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
// Check for invalid UTF-8 in email address
|
||||
const hasInvalidUtf8 = Buffer.from(command).some((byte, i, arr) => {
|
||||
if (byte >= 0x80) {
|
||||
// Check if it's valid UTF-8
|
||||
if ((byte & 0xE0) === 0xC0) {
|
||||
return i + 1 >= arr.length || (arr[i + 1] & 0xC0) !== 0x80;
|
||||
}
|
||||
// Add more UTF-8 validation as needed
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (hasInvalidUtf8) {
|
||||
socket.write('501 5.5.4 Invalid UTF-8 in address\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');
|
||||
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
|
||||
});
|
||||
|
||||
// Create email with potentially problematic content
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Test with various encodings',
|
||||
text: 'Testing text with special chars',
|
||||
headers: {
|
||||
'X-Custom-Header': 'Test value with special chars'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(` Result: ${result.messageId ? 'Success' : 'Failed'}`);
|
||||
expect(result).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 3: Base64 encoding edge cases
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing Base64 encoding edge cases`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 mail.example.com ESMTP\r\n');
|
||||
|
||||
let inData = false;
|
||||
let messageData = '';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
if (inData) {
|
||||
messageData += data.toString();
|
||||
if (messageData.includes('\r\n.\r\n')) {
|
||||
inData = false;
|
||||
|
||||
// Check for base64 content
|
||||
const base64Match = messageData.match(/Content-Transfer-Encoding:\s*base64\r?\n\r?\n([^\r\n]+)/i);
|
||||
if (base64Match) {
|
||||
const base64Content = base64Match[1];
|
||||
console.log(` [Server] Found base64 content: ${base64Content.substring(0, 50)}...`);
|
||||
|
||||
// Verify it's valid base64
|
||||
const isValidBase64 = /^[A-Za-z0-9+/]*={0,2}$/.test(base64Content.replace(/\s/g, ''));
|
||||
console.log(` [Server] Base64 valid: ${isValidBase64}`);
|
||||
}
|
||||
|
||||
socket.write('250 OK\r\n');
|
||||
messageData = '';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-mail.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
|
||||
});
|
||||
|
||||
// Create various sizes of binary content
|
||||
const sizes = [0, 1, 2, 3, 57, 58, 59, 76, 77]; // Edge cases for base64 line wrapping
|
||||
|
||||
for (const size of sizes) {
|
||||
const binaryContent = Buffer.alloc(size);
|
||||
for (let i = 0; i < size; i++) {
|
||||
binaryContent[i] = i % 256;
|
||||
}
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: `Base64 test with ${size} bytes`,
|
||||
text: 'Testing base64 encoding',
|
||||
attachments: [{
|
||||
filename: `test-${size}.bin`,
|
||||
content: binaryContent
|
||||
}]
|
||||
});
|
||||
|
||||
console.log(` Testing with ${size} byte attachment...`);
|
||||
const result = await smtpClient.sendMail(email);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 4: Quoted-printable encoding edge cases
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing quoted-printable encoding`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 mail.example.com ESMTP\r\n');
|
||||
|
||||
let inData = false;
|
||||
let messageData = '';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
if (inData) {
|
||||
messageData += data.toString();
|
||||
if (messageData.includes('\r\n.\r\n')) {
|
||||
inData = false;
|
||||
|
||||
// Check for quoted-printable content
|
||||
if (/Content-Transfer-Encoding:\s*quoted-printable/i.test(messageData)) {
|
||||
console.log(' [Server] Found quoted-printable content');
|
||||
|
||||
// Check for proper QP encoding
|
||||
const qpLines = messageData.split('\r\n');
|
||||
const longLines = qpLines.filter(line => line.length > 76);
|
||||
if (longLines.length > 0) {
|
||||
console.log(` [Server] Warning: ${longLines.length} lines exceed 76 characters`);
|
||||
}
|
||||
}
|
||||
|
||||
socket.write('250 OK\r\n');
|
||||
messageData = '';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-mail.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 with content that requires quoted-printable encoding
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Quoted-printable test',
|
||||
text: [
|
||||
'Line with special chars: café, naïve',
|
||||
'Very long line that exceeds the 76 character limit and should be properly wrapped when encoded with quoted-printable encoding',
|
||||
'Line with = sign and trailing spaces ',
|
||||
'Line ending with =',
|
||||
'Tést with various spëcial characters: ñ, ü, ß, ø, å'
|
||||
].join('\n')
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(` Result: ${result.messageId ? 'Success' : 'Failed'}`);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 5: Header encoding (RFC 2047)
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing header encoding (RFC 2047)`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 mail.example.com ESMTP\r\n');
|
||||
|
||||
let inData = false;
|
||||
let headers: string[] = [];
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const text = data.toString();
|
||||
|
||||
if (inData) {
|
||||
if (!text.startsWith('\r\n') && text.includes(':')) {
|
||||
headers.push(text.split('\r\n')[0]);
|
||||
}
|
||||
|
||||
if (text.includes('\r\n.\r\n')) {
|
||||
inData = false;
|
||||
|
||||
// Check encoded headers
|
||||
const encodedHeaders = headers.filter(h => h.includes('=?'));
|
||||
console.log(` [Server] Found ${encodedHeaders.length} encoded headers`);
|
||||
encodedHeaders.forEach(h => {
|
||||
const match = h.match(/=\?([^?]+)\?([BQ])\?([^?]+)\?=/);
|
||||
if (match) {
|
||||
console.log(` [Server] Encoded header: charset=${match[1]}, encoding=${match[2]}`);
|
||||
}
|
||||
});
|
||||
|
||||
socket.write('250 OK\r\n');
|
||||
headers = [];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const command = text.trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-mail.example.com\r\n');
|
||||
socket.write('250-SMTPUTF8\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 various header encodings
|
||||
const testCases = [
|
||||
{
|
||||
subject: 'Simple ASCII subject',
|
||||
from: { name: 'John Doe', address: 'john@example.com' }
|
||||
},
|
||||
{
|
||||
subject: 'Subject with émojis 🎉 and spéçiål çhåracters',
|
||||
from: { name: 'Jöhn Døe', address: 'john@example.com' }
|
||||
},
|
||||
{
|
||||
subject: 'Japanese: こんにちは, Chinese: 你好, Arabic: مرحبا',
|
||||
from: { name: '山田太郎', address: 'yamada@example.com' }
|
||||
},
|
||||
{
|
||||
subject: 'Very long subject that contains special characters and should be encoded and folded properly: café, naïve, résumé, piñata',
|
||||
from: { name: 'Sender with a véry løng nåme that éxceeds normal limits', address: 'sender@example.com' }
|
||||
}
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
console.log(` Testing: "${testCase.subject.substring(0, 50)}..."`);
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: testCase.from,
|
||||
to: ['recipient@example.com'],
|
||||
subject: testCase.subject,
|
||||
text: 'Testing header encoding',
|
||||
headers: {
|
||||
'X-Custom': `Custom header with special chars: ${testCase.subject.substring(0, 20)}`
|
||||
}
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 6: Content-Type charset mismatches
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing Content-Type charset handling`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 mail.example.com ESMTP\r\n');
|
||||
|
||||
let inData = false;
|
||||
|
||||
socket.on('data', (data) => {
|
||||
if (inData) {
|
||||
if (data.toString().includes('\r\n.\r\n')) {
|
||||
inData = false;
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-mail.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 with different charset declarations
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Charset test',
|
||||
text: 'Text with special chars: é, ñ, ü',
|
||||
html: '<p>HTML with different chars: café, naïve</p>',
|
||||
headers: {
|
||||
'Content-Type': 'text/plain; charset=iso-8859-1' // Mismatch with actual UTF-8 content
|
||||
}
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(` Result: ${result.messageId ? 'Success' : 'Failed'}`);
|
||||
expect(result).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
console.log(`\n${testId}: All ${scenarioCount} encoding scenarios tested ✓`);
|
||||
});
|
564
test/suite/smtpclient_edge-cases/test.cedge-06.large-headers.ts
Normal file
564
test/suite/smtpclient_edge-cases/test.cedge-06.large-headers.ts
Normal file
@@ -0,0 +1,564 @@
|
||||
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('CEDGE-06: should handle large headers gracefully', async (tools) => {
|
||||
const testId = 'CEDGE-06-large-headers';
|
||||
console.log(`\n${testId}: Testing large header handling...`);
|
||||
|
||||
let scenarioCount = 0;
|
||||
|
||||
// Scenario 1: Very long subject lines
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing very long subject lines`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 mail.example.com ESMTP\r\n');
|
||||
|
||||
let inData = false;
|
||||
let subjectLength = 0;
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const text = data.toString();
|
||||
|
||||
if (inData) {
|
||||
// Look for Subject header
|
||||
const subjectMatch = text.match(/^Subject:\s*(.+?)(?:\r\n(?:\s+.+)?)*\r\n/m);
|
||||
if (subjectMatch) {
|
||||
subjectLength = subjectMatch[0].length;
|
||||
console.log(` [Server] Subject header length: ${subjectLength} chars`);
|
||||
}
|
||||
|
||||
if (text.includes('\r\n.\r\n')) {
|
||||
inData = false;
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const command = text.trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-mail.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 various subject lengths
|
||||
const subjectLengths = [100, 500, 1000, 2000, 5000];
|
||||
|
||||
for (const length of subjectLengths) {
|
||||
const subject = 'A'.repeat(length);
|
||||
console.log(` Testing subject with ${length} characters...`);
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: subject,
|
||||
text: 'Testing long subject header folding'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 2: Many recipients (large To/Cc/Bcc headers)
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing many recipients`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 mail.example.com ESMTP\r\n');
|
||||
|
||||
let recipientCount = 0;
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('RCPT TO:')) {
|
||||
recipientCount++;
|
||||
console.log(` [Server] Recipient ${recipientCount}`);
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('EHLO')) {
|
||||
socket.write('250-mail.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
recipientCount = 0;
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
console.log(` [Server] Total recipients: ${recipientCount}`);
|
||||
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
|
||||
});
|
||||
|
||||
// Create email with many recipients
|
||||
const recipientCounts = [10, 50, 100];
|
||||
|
||||
for (const count of recipientCounts) {
|
||||
console.log(` Testing with ${count} recipients...`);
|
||||
|
||||
const toAddresses = Array(Math.floor(count / 3))
|
||||
.fill(null)
|
||||
.map((_, i) => `recipient${i + 1}@example.com`);
|
||||
|
||||
const ccAddresses = Array(Math.floor(count / 3))
|
||||
.fill(null)
|
||||
.map((_, i) => `cc${i + 1}@example.com`);
|
||||
|
||||
const bccAddresses = Array(count - toAddresses.length - ccAddresses.length)
|
||||
.fill(null)
|
||||
.map((_, i) => `bcc${i + 1}@example.com`);
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: toAddresses,
|
||||
cc: ccAddresses,
|
||||
bcc: bccAddresses,
|
||||
subject: `Test with ${count} total recipients`,
|
||||
text: 'Testing large recipient lists'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 3: Many custom headers
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing many custom headers`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 mail.example.com ESMTP\r\n');
|
||||
|
||||
let inData = false;
|
||||
let headerCount = 0;
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const text = data.toString();
|
||||
|
||||
if (inData) {
|
||||
// Count headers
|
||||
const headerLines = text.split('\r\n');
|
||||
headerLines.forEach(line => {
|
||||
if (line.match(/^[A-Za-z0-9-]+:\s*.+$/)) {
|
||||
headerCount++;
|
||||
}
|
||||
});
|
||||
|
||||
if (text.includes('\r\n.\r\n')) {
|
||||
inData = false;
|
||||
console.log(` [Server] Total headers: ${headerCount}`);
|
||||
socket.write('250 OK\r\n');
|
||||
headerCount = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const command = text.trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-mail.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
|
||||
});
|
||||
|
||||
// Create email with many headers
|
||||
const headerCounts = [10, 50, 100];
|
||||
|
||||
for (const count of headerCounts) {
|
||||
console.log(` Testing with ${count} custom headers...`);
|
||||
|
||||
const headers: { [key: string]: string } = {};
|
||||
for (let i = 0; i < count; i++) {
|
||||
headers[`X-Custom-Header-${i}`] = `This is custom header value number ${i} with some additional text to make it longer`;
|
||||
}
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: `Test with ${count} headers`,
|
||||
text: 'Testing many custom headers',
|
||||
headers
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 4: Very long header values
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing very long header values`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 mail.example.com ESMTP\r\n');
|
||||
|
||||
let inData = false;
|
||||
let maxHeaderLength = 0;
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const text = data.toString();
|
||||
|
||||
if (inData) {
|
||||
// Find longest header
|
||||
const headers = text.match(/^[A-Za-z0-9-]+:\s*.+?(?=\r\n(?:[A-Za-z0-9-]+:|$))/gms);
|
||||
if (headers) {
|
||||
headers.forEach(header => {
|
||||
if (header.length > maxHeaderLength) {
|
||||
maxHeaderLength = header.length;
|
||||
console.log(` [Server] New longest header: ${header.substring(0, 50)}... (${header.length} chars)`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (text.includes('\r\n.\r\n')) {
|
||||
inData = false;
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const command = text.trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-mail.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 with very long header values
|
||||
const longValues = [
|
||||
'A'.repeat(500),
|
||||
'Word '.repeat(200), // Many words
|
||||
Array(100).fill('item').join(', '), // Comma-separated list
|
||||
'This is a very long header value that contains multiple sentences. ' +
|
||||
'Each sentence adds to the overall length of the header. ' +
|
||||
'The header should be properly folded according to RFC 5322. ' +
|
||||
'This ensures compatibility with various email servers and clients. '.repeat(5)
|
||||
];
|
||||
|
||||
for (let i = 0; i < longValues.length; i++) {
|
||||
console.log(` Testing long header value ${i + 1} (${longValues[i].length} chars)...`);
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: `Test ${i + 1}`,
|
||||
text: 'Testing long header values',
|
||||
headers: {
|
||||
'X-Long-Header': longValues[i],
|
||||
'X-Another-Long': longValues[i].split('').reverse().join('')
|
||||
}
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 5: Headers with special folding requirements
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing header folding edge cases`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 mail.example.com ESMTP\r\n');
|
||||
|
||||
let inData = false;
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const text = data.toString();
|
||||
|
||||
if (inData) {
|
||||
// Check for proper folding (continuation lines start with whitespace)
|
||||
const lines = text.split('\r\n');
|
||||
let foldedHeaders = 0;
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
if (index > 0 && (line.startsWith(' ') || line.startsWith('\t'))) {
|
||||
foldedHeaders++;
|
||||
}
|
||||
});
|
||||
|
||||
if (foldedHeaders > 0) {
|
||||
console.log(` [Server] Found ${foldedHeaders} folded header lines`);
|
||||
}
|
||||
|
||||
if (text.includes('\r\n.\r\n')) {
|
||||
inData = false;
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const command = text.trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-mail.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 headers that require special folding
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Testing header folding',
|
||||
text: 'Testing special folding cases',
|
||||
headers: {
|
||||
// Long header with no natural break points
|
||||
'X-No-Spaces': 'A'.repeat(100),
|
||||
|
||||
// Header with URLs that shouldn't be broken
|
||||
'X-URLs': 'Visit https://example.com/very/long/path/that/should/not/be/broken/in/the/middle and https://another-example.com/another/very/long/path',
|
||||
|
||||
// Header with quoted strings
|
||||
'X-Quoted': '"This is a very long quoted string that should be kept together if possible when folding the header" and some more text',
|
||||
|
||||
// Header with structured data
|
||||
'X-Structured': 'key1=value1; key2="a very long value that might need folding"; key3=value3; key4="another long value"',
|
||||
|
||||
// References header (common to have many message IDs)
|
||||
'References': Array(20).fill(null).map((_, i) => `<message-id-${i}@example.com>`).join(' ')
|
||||
}
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(` Result: ${result.messageId ? 'Success' : 'Failed'}`);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 6: Headers at server limits
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing headers at server limits`);
|
||||
|
||||
const maxHeaderSize = 8192; // Common limit
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 mail.example.com ESMTP\r\n');
|
||||
|
||||
let inData = false;
|
||||
let headerSection = '';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const text = data.toString();
|
||||
|
||||
if (inData) {
|
||||
if (!headerSection && text.includes('\r\n\r\n')) {
|
||||
// Extract header section
|
||||
headerSection = text.substring(0, text.indexOf('\r\n\r\n'));
|
||||
console.log(` [Server] Header section size: ${headerSection.length} bytes`);
|
||||
|
||||
if (headerSection.length > maxHeaderSize) {
|
||||
socket.write('552 5.3.4 Header size exceeds maximum allowed\r\n');
|
||||
socket.end();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (text.includes('\r\n.\r\n')) {
|
||||
inData = false;
|
||||
socket.write('250 OK\r\n');
|
||||
headerSection = '';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const command = text.trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-mail.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 with headers near the limit
|
||||
const testSizes = [
|
||||
{ size: 1000, desc: 'well below limit' },
|
||||
{ size: 7000, desc: 'near limit' },
|
||||
{ size: 8000, desc: 'very close to limit' }
|
||||
];
|
||||
|
||||
for (const test of testSizes) {
|
||||
console.log(` Testing with header size ${test.desc} (${test.size} bytes)...`);
|
||||
|
||||
// Create headers that total approximately the target size
|
||||
const headers: { [key: string]: string } = {};
|
||||
let currentSize = 0;
|
||||
let headerIndex = 0;
|
||||
|
||||
while (currentSize < test.size) {
|
||||
const headerName = `X-Test-Header-${headerIndex}`;
|
||||
const remainingSize = test.size - currentSize;
|
||||
const headerValue = 'A'.repeat(Math.min(remainingSize - headerName.length - 4, 200)); // -4 for ": \r\n"
|
||||
|
||||
headers[headerName] = headerValue;
|
||||
currentSize += headerName.length + headerValue.length + 4;
|
||||
headerIndex++;
|
||||
}
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: `Testing ${test.desc}`,
|
||||
text: 'Testing header size limits',
|
||||
headers
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(` Result: Success`);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
} catch (error) {
|
||||
console.log(` Result: Failed (${error.message})`);
|
||||
// This is expected for very large headers
|
||||
}
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
console.log(`\n${testId}: All ${scenarioCount} large header scenarios tested ✓`);
|
||||
});
|
@@ -0,0 +1,634 @@
|
||||
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('CEDGE-07: should handle concurrent operations correctly', async (tools) => {
|
||||
const testId = 'CEDGE-07-concurrent-operations';
|
||||
console.log(`\n${testId}: Testing concurrent operation handling...`);
|
||||
|
||||
let scenarioCount = 0;
|
||||
|
||||
// Scenario 1: Multiple simultaneous connections
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing multiple simultaneous connections`);
|
||||
|
||||
let activeConnections = 0;
|
||||
let totalConnections = 0;
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
activeConnections++;
|
||||
totalConnections++;
|
||||
const connectionId = totalConnections;
|
||||
|
||||
console.log(` [Server] Connection ${connectionId} established (active: ${activeConnections})`);
|
||||
socket.write('220 mail.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('close', () => {
|
||||
activeConnections--;
|
||||
console.log(` [Server] Connection ${connectionId} closed (active: ${activeConnections})`);
|
||||
});
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Connection ${connectionId} received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-mail.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: Message ${connectionId} accepted\r\n`);
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Send multiple emails concurrently
|
||||
const concurrentCount = 5;
|
||||
const promises = Array(concurrentCount).fill(null).map(async (_, i) => {
|
||||
const client = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: `sender${i + 1}@example.com`,
|
||||
to: [`recipient${i + 1}@example.com`],
|
||||
subject: `Concurrent test ${i + 1}`,
|
||||
text: `This is concurrent email number ${i + 1}`
|
||||
});
|
||||
|
||||
console.log(` Starting email ${i + 1}...`);
|
||||
const start = Date.now();
|
||||
const result = await client.sendMail(email);
|
||||
const elapsed = Date.now() - start;
|
||||
console.log(` Email ${i + 1} completed in ${elapsed}ms`);
|
||||
|
||||
return { index: i + 1, result, elapsed };
|
||||
});
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
results.forEach(({ index, result, elapsed }) => {
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
console.log(` Email ${index}: Success (${elapsed}ms)`);
|
||||
});
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 2: Concurrent operations on pooled connection
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing concurrent operations on pooled connections`);
|
||||
|
||||
let connectionCount = 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] Pooled connection ${connId} established`);
|
||||
socket.write('220 mail.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-mail.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 === '.') {
|
||||
const msgCount = (connectionMessages.get(socket) || 0) + 1;
|
||||
connectionMessages.set(socket, msgCount);
|
||||
socket.write(`250 OK: Message ${msgCount} 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Create pooled client
|
||||
const pooledClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 3,
|
||||
maxMessages: 100
|
||||
});
|
||||
|
||||
// Send many emails concurrently through the pool
|
||||
const emailCount = 10;
|
||||
const promises = Array(emailCount).fill(null).map(async (_, i) => {
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i + 1}@example.com`],
|
||||
subject: `Pooled email ${i + 1}`,
|
||||
text: `Testing connection pooling with email ${i + 1}`
|
||||
});
|
||||
|
||||
const start = Date.now();
|
||||
const result = await pooledClient.sendMail(email);
|
||||
const elapsed = Date.now() - start;
|
||||
|
||||
return { index: i + 1, result, elapsed };
|
||||
});
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
let totalTime = 0;
|
||||
results.forEach(({ index, result, elapsed }) => {
|
||||
totalTime += elapsed;
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
});
|
||||
|
||||
console.log(` All ${emailCount} emails sent successfully`);
|
||||
console.log(` Average time per email: ${Math.round(totalTime / emailCount)}ms`);
|
||||
console.log(` Total connections used: ${connectionCount} (pool size: 3)`);
|
||||
|
||||
// Close pooled connections
|
||||
await pooledClient.close();
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 3: Race conditions with rapid commands
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing race conditions with rapid commands`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 mail.example.com ESMTP\r\n');
|
||||
|
||||
let commandBuffer: string[] = [];
|
||||
let processing = false;
|
||||
|
||||
const processCommand = async (command: string) => {
|
||||
// Simulate async processing with variable delays
|
||||
const delay = Math.random() * 100;
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-mail.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 === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
};
|
||||
|
||||
const processQueue = async () => {
|
||||
if (processing || commandBuffer.length === 0) return;
|
||||
processing = true;
|
||||
|
||||
while (commandBuffer.length > 0) {
|
||||
const cmd = commandBuffer.shift()!;
|
||||
console.log(` [Server] Processing: ${cmd}`);
|
||||
await processCommand(cmd);
|
||||
}
|
||||
|
||||
processing = false;
|
||||
};
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const commands = data.toString().split('\r\n').filter(cmd => cmd.length > 0);
|
||||
commands.forEach(cmd => {
|
||||
console.log(` [Server] Queued: ${cmd}`);
|
||||
commandBuffer.push(cmd);
|
||||
});
|
||||
processQueue();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Send email with rapid command sequence
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient1@example.com', 'recipient2@example.com', 'recipient3@example.com'],
|
||||
subject: 'Testing rapid commands',
|
||||
text: 'This tests race conditions with pipelined commands'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(` Result: ${result.messageId ? 'Success' : 'Failed'}`);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 4: Concurrent authentication attempts
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing concurrent authentication`);
|
||||
|
||||
let authAttempts = 0;
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 mail.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-mail.example.com\r\n');
|
||||
socket.write('250-AUTH PLAIN LOGIN\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('AUTH')) {
|
||||
authAttempts++;
|
||||
console.log(` [Server] Auth attempt ${authAttempts}`);
|
||||
|
||||
// Simulate auth processing delay
|
||||
setTimeout(() => {
|
||||
if (command.includes('PLAIN')) {
|
||||
socket.write('235 2.7.0 Authentication successful\r\n');
|
||||
} else {
|
||||
socket.write('334 VXNlcm5hbWU6\r\n'); // Username:
|
||||
}
|
||||
}, 100);
|
||||
} else if (Buffer.from(command, 'base64').toString().includes('testuser')) {
|
||||
socket.write('334 UGFzc3dvcmQ6\r\n'); // Password:
|
||||
} else if (Buffer.from(command, 'base64').toString().includes('testpass')) {
|
||||
socket.write('235 2.7.0 Authentication successful\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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Send multiple authenticated emails concurrently
|
||||
const authPromises = Array(3).fill(null).map(async (_, i) => {
|
||||
const client = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
auth: {
|
||||
user: 'testuser',
|
||||
pass: 'testpass'
|
||||
}
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i + 1}@example.com`],
|
||||
subject: `Concurrent auth test ${i + 1}`,
|
||||
text: `Testing concurrent authentication ${i + 1}`
|
||||
});
|
||||
|
||||
console.log(` Starting authenticated email ${i + 1}...`);
|
||||
const result = await client.sendMail(email);
|
||||
console.log(` Authenticated email ${i + 1} completed`);
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
const authResults = await Promise.all(authPromises);
|
||||
|
||||
authResults.forEach((result, i) => {
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
console.log(` Auth email ${i + 1}: Success`);
|
||||
});
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 5: Concurrent TLS upgrades
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing concurrent STARTTLS upgrades`);
|
||||
|
||||
let tlsUpgrades = 0;
|
||||
|
||||
const testServer = await createTestServer({
|
||||
secure: false,
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 mail.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-mail.example.com\r\n');
|
||||
socket.write('250-STARTTLS\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'STARTTLS') {
|
||||
tlsUpgrades++;
|
||||
console.log(` [Server] TLS upgrade ${tlsUpgrades}`);
|
||||
socket.write('220 2.0.0 Ready to start TLS\r\n');
|
||||
|
||||
// Note: In real test, would upgrade to TLS here
|
||||
// For this test, we'll continue in plain text
|
||||
} 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Send multiple emails with STARTTLS concurrently
|
||||
const tlsPromises = Array(3).fill(null).map(async (_, i) => {
|
||||
const client = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
requireTLS: false // Would be true in production
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i + 1}@example.com`],
|
||||
subject: `TLS upgrade test ${i + 1}`,
|
||||
text: `Testing concurrent TLS upgrades ${i + 1}`
|
||||
});
|
||||
|
||||
console.log(` Starting TLS email ${i + 1}...`);
|
||||
const result = await client.sendMail(email);
|
||||
console.log(` TLS email ${i + 1} completed`);
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
const tlsResults = await Promise.all(tlsPromises);
|
||||
|
||||
tlsResults.forEach((result, i) => {
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
});
|
||||
|
||||
console.log(` Total TLS upgrades: ${tlsUpgrades}`);
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 6: Mixed concurrent operations
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing mixed concurrent operations`);
|
||||
|
||||
const stats = {
|
||||
connections: 0,
|
||||
messages: 0,
|
||||
errors: 0,
|
||||
timeouts: 0
|
||||
};
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
stats.connections++;
|
||||
const connId = stats.connections;
|
||||
|
||||
console.log(` [Server] Connection ${connId} established`);
|
||||
socket.write('220 mail.example.com ESMTP\r\n');
|
||||
|
||||
let messageInProgress = false;
|
||||
|
||||
socket.on('data', async (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
// Simulate various server behaviors
|
||||
const behavior = connId % 4;
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
if (behavior === 0) {
|
||||
// Normal response
|
||||
socket.write('250-mail.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (behavior === 1) {
|
||||
// Slow response
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
socket.write('250-mail.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (behavior === 2) {
|
||||
// Temporary error
|
||||
socket.write('421 4.3.2 Service temporarily unavailable\r\n');
|
||||
stats.errors++;
|
||||
socket.end();
|
||||
} else {
|
||||
// Normal with extensions
|
||||
socket.write('250-mail.example.com\r\n');
|
||||
socket.write('250-PIPELINING\r\n');
|
||||
socket.write('250-SIZE 10485760\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
messageInProgress = true;
|
||||
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 === '.') {
|
||||
if (messageInProgress) {
|
||||
stats.messages++;
|
||||
messageInProgress = false;
|
||||
}
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
|
||||
// Simulate connection timeout for some connections
|
||||
if (behavior === 3) {
|
||||
setTimeout(() => {
|
||||
if (!socket.destroyed) {
|
||||
console.log(` [Server] Connection ${connId} timed out`);
|
||||
stats.timeouts++;
|
||||
socket.destroy();
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Send various types of operations concurrently
|
||||
const operations = [
|
||||
// Normal emails
|
||||
...Array(5).fill(null).map((_, i) => ({
|
||||
type: 'normal',
|
||||
index: i,
|
||||
action: async () => {
|
||||
const client = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i + 1}@example.com`],
|
||||
subject: `Normal email ${i + 1}`,
|
||||
text: 'Testing mixed operations'
|
||||
});
|
||||
|
||||
return await client.sendMail(email);
|
||||
}
|
||||
})),
|
||||
|
||||
// Large emails
|
||||
...Array(2).fill(null).map((_, i) => ({
|
||||
type: 'large',
|
||||
index: i,
|
||||
action: async () => {
|
||||
const client = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: `Large email ${i + 1}`,
|
||||
text: 'X'.repeat(100000) // 100KB
|
||||
});
|
||||
|
||||
return await client.sendMail(email);
|
||||
}
|
||||
})),
|
||||
|
||||
// Multiple recipient emails
|
||||
...Array(3).fill(null).map((_, i) => ({
|
||||
type: 'multi',
|
||||
index: i,
|
||||
action: async () => {
|
||||
const client = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: Array(10).fill(null).map((_, j) => `recipient${j + 1}@example.com`),
|
||||
subject: `Multi-recipient email ${i + 1}`,
|
||||
text: 'Testing multiple recipients'
|
||||
});
|
||||
|
||||
return await client.sendMail(email);
|
||||
}
|
||||
}))
|
||||
];
|
||||
|
||||
console.log(` Starting ${operations.length} mixed operations...`);
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
operations.map(async (op) => {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const result = await op.action();
|
||||
const elapsed = Date.now() - start;
|
||||
return { ...op, success: true, elapsed, result };
|
||||
} catch (error) {
|
||||
const elapsed = Date.now() - start;
|
||||
return { ...op, success: false, elapsed, error: error.message };
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Analyze results
|
||||
const summary = {
|
||||
normal: { success: 0, failed: 0 },
|
||||
large: { success: 0, failed: 0 },
|
||||
multi: { success: 0, failed: 0 }
|
||||
};
|
||||
|
||||
results.forEach((result) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
const { type, success, elapsed } = result.value;
|
||||
if (success) {
|
||||
summary[type].success++;
|
||||
} else {
|
||||
summary[type].failed++;
|
||||
}
|
||||
console.log(` ${type} operation: ${success ? 'Success' : 'Failed'} (${elapsed}ms)`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('\n Summary:');
|
||||
console.log(` - Normal emails: ${summary.normal.success}/${summary.normal.success + summary.normal.failed} successful`);
|
||||
console.log(` - Large emails: ${summary.large.success}/${summary.large.success + summary.large.failed} successful`);
|
||||
console.log(` - Multi-recipient: ${summary.multi.success}/${summary.multi.success + summary.multi.failed} successful`);
|
||||
console.log(` - Server stats: ${stats.connections} connections, ${stats.messages} messages, ${stats.errors} errors, ${stats.timeouts} timeouts`);
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
console.log(`\n${testId}: All ${scenarioCount} concurrent operation scenarios tested ✓`);
|
||||
});
|
Reference in New Issue
Block a user