update
This commit is contained in:
parent
4e4c7df558
commit
11a2ae6b27
@ -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 ✓`);
|
||||
});
|
@ -0,0 +1,562 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { startTestSmtpServer } from '../../helpers/server.loader.js';
|
||||
import { createSmtpClient } from '../../helpers/smtp.client.js';
|
||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
||||
import * as net from 'net';
|
||||
|
||||
let testServer: any;
|
||||
|
||||
tap.test('setup test SMTP server', async () => {
|
||||
testServer = await startTestSmtpServer();
|
||||
expect(testServer).toBeTruthy();
|
||||
expect(testServer.port).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('CERR-07: SIZE extension detection', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
// Check for SIZE extension
|
||||
const ehloResponse = await smtpClient.sendCommand('EHLO testclient.example.com');
|
||||
console.log('\nChecking SIZE extension support...');
|
||||
|
||||
const sizeMatch = ehloResponse.match(/250[- ]SIZE (\d+)/);
|
||||
if (sizeMatch) {
|
||||
const maxSize = parseInt(sizeMatch[1]);
|
||||
console.log(`Server advertises SIZE extension: ${maxSize} bytes`);
|
||||
console.log(` Human readable: ${(maxSize / 1024 / 1024).toFixed(2)} MB`);
|
||||
|
||||
// Common size limits
|
||||
const commonLimits = [
|
||||
{ size: 10 * 1024 * 1024, name: '10 MB' },
|
||||
{ size: 25 * 1024 * 1024, name: '25 MB' },
|
||||
{ size: 50 * 1024 * 1024, name: '50 MB' },
|
||||
{ size: 100 * 1024 * 1024, name: '100 MB' }
|
||||
];
|
||||
|
||||
const closestLimit = commonLimits.find(limit => Math.abs(limit.size - maxSize) < 1024 * 1024);
|
||||
if (closestLimit) {
|
||||
console.log(` Appears to be standard ${closestLimit.name} limit`);
|
||||
}
|
||||
} else {
|
||||
console.log('Server does not advertise SIZE extension');
|
||||
}
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-07: Message size calculation', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
// Test different message components and their size impact
|
||||
console.log('\nMessage size calculation tests:');
|
||||
|
||||
const sizeTests = [
|
||||
{
|
||||
name: 'Plain text only',
|
||||
email: new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Size test',
|
||||
text: 'x'.repeat(1000)
|
||||
}),
|
||||
expectedSize: 1200 // Approximate with headers
|
||||
},
|
||||
{
|
||||
name: 'HTML content',
|
||||
email: new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'HTML size test',
|
||||
html: '<html><body>' + 'x'.repeat(1000) + '</body></html>',
|
||||
text: 'x'.repeat(1000)
|
||||
}),
|
||||
expectedSize: 2500 // Multipart adds overhead
|
||||
},
|
||||
{
|
||||
name: 'With attachment',
|
||||
email: new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Attachment test',
|
||||
text: 'See attachment',
|
||||
attachments: [{
|
||||
filename: 'test.txt',
|
||||
content: Buffer.from('x'.repeat(10000)),
|
||||
contentType: 'text/plain'
|
||||
}]
|
||||
}),
|
||||
expectedSize: 14000 // Base64 encoding adds ~33%
|
||||
}
|
||||
];
|
||||
|
||||
for (const test of sizeTests) {
|
||||
// Calculate actual message size
|
||||
let messageSize = 0;
|
||||
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
|
||||
|
||||
smtpClient.sendCommand = async (command: string) => {
|
||||
messageSize += Buffer.byteLength(command, 'utf8');
|
||||
|
||||
// Check SIZE parameter in MAIL FROM
|
||||
if (command.startsWith('MAIL FROM') && command.includes('SIZE=')) {
|
||||
const sizeMatch = command.match(/SIZE=(\d+)/);
|
||||
if (sizeMatch) {
|
||||
console.log(`\n${test.name}:`);
|
||||
console.log(` SIZE parameter: ${sizeMatch[1]} bytes`);
|
||||
}
|
||||
}
|
||||
|
||||
return originalSendCommand(command);
|
||||
};
|
||||
|
||||
try {
|
||||
await smtpClient.sendMail(test.email);
|
||||
console.log(` Actual transmitted: ${messageSize} bytes`);
|
||||
console.log(` Expected (approx): ${test.expectedSize} bytes`);
|
||||
} catch (error) {
|
||||
console.log(` Error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-07: Exceeding size limits', async () => {
|
||||
// Create server with size limit
|
||||
const sizeLimitServer = net.createServer((socket) => {
|
||||
const maxSize = 1024 * 1024; // 1 MB limit
|
||||
let currentMailSize = 0;
|
||||
let inData = false;
|
||||
|
||||
socket.write('220 Size Limit Test Server\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString();
|
||||
|
||||
if (command.trim().startsWith('EHLO')) {
|
||||
socket.write(`250-sizelimit.example.com\r\n`);
|
||||
socket.write(`250-SIZE ${maxSize}\r\n`);
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.trim().startsWith('MAIL FROM')) {
|
||||
// Check SIZE parameter
|
||||
const sizeMatch = command.match(/SIZE=(\d+)/);
|
||||
if (sizeMatch) {
|
||||
const declaredSize = parseInt(sizeMatch[1]);
|
||||
if (declaredSize > maxSize) {
|
||||
socket.write(`552 5.3.4 Message size exceeds fixed maximum message size (${maxSize})\r\n`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
currentMailSize = 0;
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.trim().startsWith('RCPT TO')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.trim() === 'DATA') {
|
||||
inData = true;
|
||||
socket.write('354 Send data\r\n');
|
||||
} else if (inData) {
|
||||
currentMailSize += Buffer.byteLength(command, 'utf8');
|
||||
|
||||
if (command.trim() === '.') {
|
||||
inData = false;
|
||||
if (currentMailSize > maxSize) {
|
||||
socket.write(`552 5.3.4 Message too big (${currentMailSize} bytes, limit is ${maxSize})\r\n`);
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
}
|
||||
} else if (command.trim() === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
sizeLimitServer.listen(0, '127.0.0.1', () => resolve());
|
||||
});
|
||||
|
||||
const sizeLimitPort = (sizeLimitServer.address() as net.AddressInfo).port;
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: sizeLimitPort,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
console.log('\nTesting size limit enforcement (1 MB limit)...');
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
// Test messages of different sizes
|
||||
const sizes = [
|
||||
{ size: 500 * 1024, name: '500 KB', shouldSucceed: true },
|
||||
{ size: 900 * 1024, name: '900 KB', shouldSucceed: true },
|
||||
{ size: 1.5 * 1024 * 1024, name: '1.5 MB', shouldSucceed: false },
|
||||
{ size: 5 * 1024 * 1024, name: '5 MB', shouldSucceed: false }
|
||||
];
|
||||
|
||||
for (const test of sizes) {
|
||||
console.log(`\nTesting ${test.name} message...`);
|
||||
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: `Size test: ${test.name}`,
|
||||
text: 'x'.repeat(test.size)
|
||||
});
|
||||
|
||||
try {
|
||||
await smtpClient.sendMail(email);
|
||||
if (test.shouldSucceed) {
|
||||
console.log(' ✓ Accepted as expected');
|
||||
} else {
|
||||
console.log(' ✗ Unexpectedly accepted');
|
||||
}
|
||||
} catch (error) {
|
||||
if (!test.shouldSucceed) {
|
||||
console.log(' ✓ Rejected as expected:', error.message);
|
||||
expect(error.message).toMatch(/552|size|big|large|exceed/i);
|
||||
} else {
|
||||
console.log(' ✗ Unexpectedly rejected:', error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await smtpClient.close();
|
||||
sizeLimitServer.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-07: Size rejection at different stages', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
console.log('\nSize rejection can occur at different stages:');
|
||||
|
||||
// 1. MAIL FROM with SIZE parameter
|
||||
console.log('\n1. During MAIL FROM (with SIZE parameter):');
|
||||
try {
|
||||
await smtpClient.sendCommand('MAIL FROM:<sender@example.com> SIZE=999999999');
|
||||
console.log(' Large SIZE accepted in MAIL FROM');
|
||||
} catch (error) {
|
||||
console.log(' Rejected at MAIL FROM:', error.message);
|
||||
}
|
||||
await smtpClient.sendCommand('RSET');
|
||||
|
||||
// 2. After DATA command
|
||||
console.log('\n2. After receiving message data:');
|
||||
const largeEmail = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Large message',
|
||||
text: 'x'.repeat(10 * 1024 * 1024) // 10 MB
|
||||
});
|
||||
|
||||
try {
|
||||
await smtpClient.sendMail(largeEmail);
|
||||
console.log(' Large message accepted');
|
||||
} catch (error) {
|
||||
console.log(' Rejected after DATA:', error.message);
|
||||
}
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-07: Attachment encoding overhead', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
console.log('\nTesting attachment encoding overhead:');
|
||||
|
||||
// Test how different content types affect size
|
||||
const attachmentTests = [
|
||||
{
|
||||
name: 'Binary file (base64)',
|
||||
content: Buffer.from(Array(1000).fill(0xFF)),
|
||||
encoding: 'base64',
|
||||
overhead: 1.33 // ~33% overhead
|
||||
},
|
||||
{
|
||||
name: 'Text file (quoted-printable)',
|
||||
content: Buffer.from('This is plain text content.\r\n'.repeat(100)),
|
||||
encoding: 'quoted-printable',
|
||||
overhead: 1.1 // ~10% overhead for mostly ASCII
|
||||
},
|
||||
{
|
||||
name: 'Already base64',
|
||||
content: Buffer.from('SGVsbG8gV29ybGQh'.repeat(100)),
|
||||
encoding: '7bit',
|
||||
overhead: 1.0 // No additional encoding
|
||||
}
|
||||
];
|
||||
|
||||
for (const test of attachmentTests) {
|
||||
const originalSize = test.content.length;
|
||||
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: `Encoding test: ${test.name}`,
|
||||
text: 'See attachment',
|
||||
attachments: [{
|
||||
filename: 'test.dat',
|
||||
content: test.content,
|
||||
encoding: test.encoding as any
|
||||
}]
|
||||
});
|
||||
|
||||
// Monitor actual transmitted size
|
||||
let transmittedSize = 0;
|
||||
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
|
||||
|
||||
smtpClient.sendCommand = async (command: string) => {
|
||||
transmittedSize += Buffer.byteLength(command, 'utf8');
|
||||
return originalSendCommand(command);
|
||||
};
|
||||
|
||||
await smtpClient.sendMail(email);
|
||||
|
||||
const attachmentSize = transmittedSize - 1000; // Rough estimate minus headers
|
||||
const actualOverhead = attachmentSize / originalSize;
|
||||
|
||||
console.log(`\n${test.name}:`);
|
||||
console.log(` Original size: ${originalSize} bytes`);
|
||||
console.log(` Transmitted size: ~${attachmentSize} bytes`);
|
||||
console.log(` Actual overhead: ${(actualOverhead * 100 - 100).toFixed(1)}%`);
|
||||
console.log(` Expected overhead: ${(test.overhead * 100 - 100).toFixed(1)}%`);
|
||||
}
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-07: Chunked transfer for large messages', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 30000,
|
||||
chunkSize: 64 * 1024, // 64KB chunks
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
console.log('\nTesting chunked transfer for large message...');
|
||||
|
||||
// Create a large message
|
||||
const chunkSize = 64 * 1024;
|
||||
const totalSize = 2 * 1024 * 1024; // 2 MB
|
||||
const chunks = Math.ceil(totalSize / chunkSize);
|
||||
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Chunked transfer test',
|
||||
text: 'x'.repeat(totalSize)
|
||||
});
|
||||
|
||||
// Monitor chunk transmission
|
||||
let chunkCount = 0;
|
||||
let bytesSent = 0;
|
||||
const startTime = Date.now();
|
||||
|
||||
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
|
||||
|
||||
smtpClient.sendCommand = async (command: string) => {
|
||||
const commandSize = Buffer.byteLength(command, 'utf8');
|
||||
bytesSent += commandSize;
|
||||
|
||||
// Detect chunk boundaries (simplified)
|
||||
if (commandSize > 1000 && commandSize <= chunkSize + 100) {
|
||||
chunkCount++;
|
||||
const progress = (bytesSent / totalSize * 100).toFixed(1);
|
||||
console.log(` Chunk ${chunkCount}: ${commandSize} bytes (${progress}% complete)`);
|
||||
}
|
||||
|
||||
return originalSendCommand(command);
|
||||
};
|
||||
|
||||
await smtpClient.sendMail(email);
|
||||
|
||||
const elapsed = Date.now() - startTime;
|
||||
const throughput = (bytesSent / elapsed * 1000 / 1024).toFixed(2);
|
||||
|
||||
console.log(`\nTransfer complete:`);
|
||||
console.log(` Total chunks: ${chunkCount}`);
|
||||
console.log(` Total bytes: ${bytesSent}`);
|
||||
console.log(` Time: ${elapsed}ms`);
|
||||
console.log(` Throughput: ${throughput} KB/s`);
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-07: Size limit error recovery', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
autoShrinkAttachments: true, // Automatically compress/resize attachments
|
||||
maxMessageSize: 5 * 1024 * 1024, // 5 MB client-side limit
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
console.log('\nTesting size limit error recovery...');
|
||||
|
||||
// Create oversized email
|
||||
const largeImage = Buffer.alloc(10 * 1024 * 1024); // 10 MB "image"
|
||||
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Large attachment',
|
||||
text: 'See attached image',
|
||||
attachments: [{
|
||||
filename: 'large-image.jpg',
|
||||
content: largeImage,
|
||||
contentType: 'image/jpeg'
|
||||
}]
|
||||
});
|
||||
|
||||
// Monitor size reduction attempts
|
||||
smtpClient.on('attachment-resize', (info) => {
|
||||
console.log(`\nAttempting to reduce attachment size:`);
|
||||
console.log(` Original: ${info.originalSize} bytes`);
|
||||
console.log(` Target: ${info.targetSize} bytes`);
|
||||
console.log(` Method: ${info.method}`);
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log('\nEmail sent after size reduction');
|
||||
|
||||
if (result.modifications) {
|
||||
console.log('Modifications made:');
|
||||
result.modifications.forEach(mod => {
|
||||
console.log(` - ${mod}`);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('\nFailed even after size reduction:', error.message);
|
||||
}
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-07: Multiple size limits', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
console.log('\nDifferent types of size limits:');
|
||||
|
||||
const sizeLimits = [
|
||||
{
|
||||
type: 'Total message size',
|
||||
limit: '25 MB',
|
||||
description: 'Complete MIME message including all parts'
|
||||
},
|
||||
{
|
||||
type: 'Individual attachment',
|
||||
limit: '10 MB',
|
||||
description: 'Per-attachment limit'
|
||||
},
|
||||
{
|
||||
type: 'Text content',
|
||||
limit: '1 MB',
|
||||
description: 'Plain text or HTML body'
|
||||
},
|
||||
{
|
||||
type: 'Header size',
|
||||
limit: '100 KB',
|
||||
description: 'Total size of all headers'
|
||||
},
|
||||
{
|
||||
type: 'Recipient count',
|
||||
limit: '100',
|
||||
description: 'Affects total message size with BCC expansion'
|
||||
}
|
||||
];
|
||||
|
||||
sizeLimits.forEach(limit => {
|
||||
console.log(`\n${limit.type}:`);
|
||||
console.log(` Typical limit: ${limit.limit}`);
|
||||
console.log(` Description: ${limit.description}`);
|
||||
});
|
||||
|
||||
// Test cumulative size with multiple attachments
|
||||
console.log('\n\nTesting cumulative attachment size...');
|
||||
|
||||
const attachments = Array.from({ length: 5 }, (_, i) => ({
|
||||
filename: `file${i + 1}.dat`,
|
||||
content: Buffer.alloc(2 * 1024 * 1024), // 2 MB each
|
||||
contentType: 'application/octet-stream'
|
||||
}));
|
||||
|
||||
const multiAttachEmail = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Multiple attachments',
|
||||
text: 'Testing cumulative size',
|
||||
attachments: attachments
|
||||
});
|
||||
|
||||
console.log(`Total attachment size: ${attachments.length * 2} MB`);
|
||||
|
||||
try {
|
||||
await smtpClient.sendMail(multiAttachEmail);
|
||||
console.log('Multiple attachments accepted');
|
||||
} catch (error) {
|
||||
console.log('Rejected due to cumulative size:', error.message);
|
||||
}
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('cleanup test SMTP server', async () => {
|
||||
if (testServer) {
|
||||
await testServer.stop();
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
@ -0,0 +1,573 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { startTestSmtpServer } from '../../helpers/server.loader.js';
|
||||
import { createSmtpClient } from '../../helpers/smtp.client.js';
|
||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
||||
import * as net from 'net';
|
||||
|
||||
let testServer: any;
|
||||
|
||||
tap.test('setup test SMTP server', async () => {
|
||||
testServer = await startTestSmtpServer();
|
||||
expect(testServer).toBeTruthy();
|
||||
expect(testServer.port).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('CERR-08: Connection rate limiting', async () => {
|
||||
// Create server with connection rate limiting
|
||||
let connectionCount = 0;
|
||||
let connectionTimes: number[] = [];
|
||||
const maxConnectionsPerMinute = 10;
|
||||
|
||||
const rateLimitServer = net.createServer((socket) => {
|
||||
const now = Date.now();
|
||||
connectionTimes.push(now);
|
||||
connectionCount++;
|
||||
|
||||
// Remove old connection times (older than 1 minute)
|
||||
connectionTimes = connectionTimes.filter(time => now - time < 60000);
|
||||
|
||||
if (connectionTimes.length > maxConnectionsPerMinute) {
|
||||
socket.write('421 4.7.0 Too many connections, please try again later\r\n');
|
||||
socket.end();
|
||||
return;
|
||||
}
|
||||
|
||||
socket.write('220 Rate Limit Test Server\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
rateLimitServer.listen(0, '127.0.0.1', () => resolve());
|
||||
});
|
||||
|
||||
const rateLimitPort = (rateLimitServer.address() as net.AddressInfo).port;
|
||||
|
||||
console.log('\nTesting connection rate limiting...');
|
||||
console.log(`Server limit: ${maxConnectionsPerMinute} connections per minute`);
|
||||
|
||||
// Try to make many connections rapidly
|
||||
const connections: any[] = [];
|
||||
let accepted = 0;
|
||||
let rejected = 0;
|
||||
|
||||
for (let i = 0; i < 15; i++) {
|
||||
try {
|
||||
const client = createSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: rateLimitPort,
|
||||
secure: false,
|
||||
connectionTimeout: 2000,
|
||||
debug: false
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
accepted++;
|
||||
connections.push(client);
|
||||
console.log(` Connection ${i + 1}: Accepted`);
|
||||
} catch (error) {
|
||||
rejected++;
|
||||
console.log(` Connection ${i + 1}: Rejected - ${error.message}`);
|
||||
expect(error.message).toMatch(/421|too many|rate/i);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nResults: ${accepted} accepted, ${rejected} rejected`);
|
||||
expect(rejected).toBeGreaterThan(0); // Some should be rate limited
|
||||
|
||||
// Clean up connections
|
||||
for (const client of connections) {
|
||||
await client.close();
|
||||
}
|
||||
|
||||
rateLimitServer.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-08: Message rate limiting', async () => {
|
||||
// Create server with message rate limiting
|
||||
const messageRateLimits: { [key: string]: { count: number; resetTime: number } } = {};
|
||||
const messagesPerHour = 100;
|
||||
|
||||
const messageRateLimitServer = net.createServer((socket) => {
|
||||
let senderAddress = '';
|
||||
|
||||
socket.write('220 Message Rate Limit Server\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM')) {
|
||||
const match = command.match(/<([^>]+)>/);
|
||||
if (match) {
|
||||
senderAddress = match[1];
|
||||
const now = Date.now();
|
||||
|
||||
if (!messageRateLimits[senderAddress]) {
|
||||
messageRateLimits[senderAddress] = { count: 0, resetTime: now + 3600000 };
|
||||
}
|
||||
|
||||
// Reset if hour has passed
|
||||
if (now > messageRateLimits[senderAddress].resetTime) {
|
||||
messageRateLimits[senderAddress] = { count: 0, resetTime: now + 3600000 };
|
||||
}
|
||||
|
||||
messageRateLimits[senderAddress].count++;
|
||||
|
||||
if (messageRateLimits[senderAddress].count > messagesPerHour) {
|
||||
socket.write(`421 4.7.0 Message rate limit exceeded (${messagesPerHour}/hour)\r\n`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
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 Send data\r\n');
|
||||
} else if (command === '.') {
|
||||
const remaining = messagesPerHour - messageRateLimits[senderAddress].count;
|
||||
socket.write(`250 OK (${remaining} messages remaining this hour)\r\n`);
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
messageRateLimitServer.listen(0, '127.0.0.1', () => resolve());
|
||||
});
|
||||
|
||||
const messageRateLimitPort = (messageRateLimitServer.address() as net.AddressInfo).port;
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: messageRateLimitPort,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
console.log('\nTesting message rate limiting...');
|
||||
console.log(`Server limit: ${messagesPerHour} messages per hour per sender`);
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
// Simulate sending many messages
|
||||
const testMessageCount = 10;
|
||||
const sender = 'bulk-sender@example.com';
|
||||
|
||||
for (let i = 0; i < testMessageCount; i++) {
|
||||
const email = new Email({
|
||||
from: sender,
|
||||
to: [`recipient${i}@example.com`],
|
||||
subject: `Test message ${i + 1}`,
|
||||
text: 'Testing message rate limits'
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await smtpClient.sendMail(email);
|
||||
|
||||
// Extract remaining count from response
|
||||
const remainingMatch = result.response?.match(/(\d+) messages remaining/);
|
||||
if (remainingMatch) {
|
||||
console.log(` Message ${i + 1}: Sent (${remainingMatch[1]} remaining)`);
|
||||
} else {
|
||||
console.log(` Message ${i + 1}: Sent`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(` Message ${i + 1}: Rate limited - ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
await smtpClient.close();
|
||||
messageRateLimitServer.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-08: Recipient rate limiting', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
console.log('\nTesting recipient rate limiting...');
|
||||
|
||||
// Test different recipient rate limit scenarios
|
||||
const recipientTests = [
|
||||
{
|
||||
name: 'Many recipients in single message',
|
||||
recipients: Array.from({ length: 200 }, (_, i) => `user${i}@example.com`),
|
||||
expectedLimit: 100
|
||||
},
|
||||
{
|
||||
name: 'Rapid sequential messages',
|
||||
recipients: Array.from({ length: 50 }, (_, i) => `rapid${i}@example.com`),
|
||||
delay: 0
|
||||
}
|
||||
];
|
||||
|
||||
for (const test of recipientTests) {
|
||||
console.log(`\n${test.name}:`);
|
||||
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: test.recipients,
|
||||
subject: test.name,
|
||||
text: 'Testing recipient limits'
|
||||
});
|
||||
|
||||
let acceptedCount = 0;
|
||||
let rejectedCount = 0;
|
||||
|
||||
// Monitor RCPT TO responses
|
||||
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
|
||||
|
||||
smtpClient.sendCommand = async (command: string) => {
|
||||
const response = await originalSendCommand(command);
|
||||
|
||||
if (command.startsWith('RCPT TO')) {
|
||||
if (response.startsWith('250')) {
|
||||
acceptedCount++;
|
||||
} else if (response.match(/^[45]/)) {
|
||||
rejectedCount++;
|
||||
|
||||
if (response.match(/rate|limit|too many|slow down/i)) {
|
||||
console.log(` Rate limit hit after ${acceptedCount} recipients`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
try {
|
||||
await smtpClient.sendMail(email);
|
||||
console.log(` All ${acceptedCount} recipients accepted`);
|
||||
} catch (error) {
|
||||
console.log(` Accepted: ${acceptedCount}, Rejected: ${rejectedCount}`);
|
||||
console.log(` Error: ${error.message}`);
|
||||
}
|
||||
|
||||
await smtpClient.sendCommand('RSET');
|
||||
}
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-08: Rate limit response codes', async () => {
|
||||
console.log('\nCommon rate limiting response codes:');
|
||||
|
||||
const rateLimitCodes = [
|
||||
{
|
||||
code: '421 4.7.0',
|
||||
message: 'Too many connections',
|
||||
type: 'Connection rate limit',
|
||||
action: 'Close connection, retry later'
|
||||
},
|
||||
{
|
||||
code: '450 4.7.1',
|
||||
message: 'Rate limit exceeded, try again later',
|
||||
type: 'Command rate limit',
|
||||
action: 'Temporary failure, queue and retry'
|
||||
},
|
||||
{
|
||||
code: '451 4.7.1',
|
||||
message: 'Please slow down',
|
||||
type: 'Throttling request',
|
||||
action: 'Add delay before next command'
|
||||
},
|
||||
{
|
||||
code: '452 4.5.3',
|
||||
message: 'Too many recipients',
|
||||
type: 'Recipient limit',
|
||||
action: 'Split into multiple messages'
|
||||
},
|
||||
{
|
||||
code: '454 4.7.0',
|
||||
message: 'Temporary authentication failure',
|
||||
type: 'Auth rate limit',
|
||||
action: 'Delay and retry authentication'
|
||||
},
|
||||
{
|
||||
code: '550 5.7.1',
|
||||
message: 'Daily sending quota exceeded',
|
||||
type: 'Hard quota limit',
|
||||
action: 'Stop sending until quota resets'
|
||||
}
|
||||
];
|
||||
|
||||
rateLimitCodes.forEach(limit => {
|
||||
console.log(`\n${limit.code} ${limit.message}`);
|
||||
console.log(` Type: ${limit.type}`);
|
||||
console.log(` Action: ${limit.action}`);
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('CERR-08: Adaptive rate limiting', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
adaptiveRateLimit: true,
|
||||
initialDelay: 100, // Start with 100ms between commands
|
||||
maxDelay: 5000, // Max 5 seconds between commands
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
console.log('\nTesting adaptive rate limiting...');
|
||||
|
||||
// Track delays
|
||||
const delays: number[] = [];
|
||||
let lastCommandTime = Date.now();
|
||||
|
||||
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
|
||||
|
||||
smtpClient.sendCommand = async (command: string) => {
|
||||
const now = Date.now();
|
||||
const delay = now - lastCommandTime;
|
||||
delays.push(delay);
|
||||
lastCommandTime = now;
|
||||
|
||||
return originalSendCommand(command);
|
||||
};
|
||||
|
||||
// Send multiple emails and observe delay adaptation
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i}@example.com`],
|
||||
subject: `Adaptive test ${i + 1}`,
|
||||
text: 'Testing adaptive rate limiting'
|
||||
});
|
||||
|
||||
try {
|
||||
await smtpClient.sendMail(email);
|
||||
console.log(` Email ${i + 1}: Sent with ${delays[delays.length - 1]}ms delay`);
|
||||
} catch (error) {
|
||||
console.log(` Email ${i + 1}: Failed - ${error.message}`);
|
||||
|
||||
// Check if delay increased
|
||||
if (delays.length > 1) {
|
||||
const lastDelay = delays[delays.length - 1];
|
||||
const previousDelay = delays[delays.length - 2];
|
||||
if (lastDelay > previousDelay) {
|
||||
console.log(` Delay increased from ${previousDelay}ms to ${lastDelay}ms`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-08: Rate limit headers', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
console.log('\nChecking for rate limit information in responses...');
|
||||
|
||||
// Send email and monitor for rate limit headers
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Rate limit header test',
|
||||
text: 'Checking for rate limit information'
|
||||
});
|
||||
|
||||
// Monitor responses for rate limit info
|
||||
const rateLimitInfo: string[] = [];
|
||||
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
|
||||
|
||||
smtpClient.sendCommand = async (command: string) => {
|
||||
const response = await originalSendCommand(command);
|
||||
|
||||
// Look for rate limit information in responses
|
||||
const patterns = [
|
||||
/X-RateLimit-Limit: (\d+)/i,
|
||||
/X-RateLimit-Remaining: (\d+)/i,
|
||||
/X-RateLimit-Reset: (\d+)/i,
|
||||
/(\d+) requests? remaining/i,
|
||||
/limit.* (\d+) per/i,
|
||||
/retry.* (\d+) seconds?/i
|
||||
];
|
||||
|
||||
patterns.forEach(pattern => {
|
||||
const match = response.match(pattern);
|
||||
if (match) {
|
||||
rateLimitInfo.push(match[0]);
|
||||
}
|
||||
});
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
await smtpClient.sendMail(email);
|
||||
|
||||
if (rateLimitInfo.length > 0) {
|
||||
console.log('Rate limit information found:');
|
||||
rateLimitInfo.forEach(info => console.log(` ${info}`));
|
||||
} else {
|
||||
console.log('No rate limit information in responses');
|
||||
}
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-08: Distributed rate limiting', async () => {
|
||||
console.log('\nDistributed rate limiting strategies:');
|
||||
|
||||
const strategies = [
|
||||
{
|
||||
name: 'Token bucket',
|
||||
description: 'Fixed number of tokens replenished at constant rate',
|
||||
pros: 'Allows bursts, smooth rate control',
|
||||
cons: 'Can be complex to implement distributed'
|
||||
},
|
||||
{
|
||||
name: 'Sliding window',
|
||||
description: 'Count requests in moving time window',
|
||||
pros: 'More accurate than fixed windows',
|
||||
cons: 'Higher memory usage'
|
||||
},
|
||||
{
|
||||
name: 'Fixed window',
|
||||
description: 'Reset counter at fixed intervals',
|
||||
pros: 'Simple to implement',
|
||||
cons: 'Can allow 2x rate at window boundaries'
|
||||
},
|
||||
{
|
||||
name: 'Leaky bucket',
|
||||
description: 'Queue with constant drain rate',
|
||||
pros: 'Smooth output rate',
|
||||
cons: 'Can drop messages if bucket overflows'
|
||||
}
|
||||
];
|
||||
|
||||
strategies.forEach(strategy => {
|
||||
console.log(`\n${strategy.name}:`);
|
||||
console.log(` Description: ${strategy.description}`);
|
||||
console.log(` Pros: ${strategy.pros}`);
|
||||
console.log(` Cons: ${strategy.cons}`);
|
||||
});
|
||||
|
||||
// Simulate distributed rate limiting
|
||||
const distributedLimiter = {
|
||||
nodes: ['server1', 'server2', 'server3'],
|
||||
globalLimit: 1000, // 1000 messages per minute globally
|
||||
perNodeLimit: 400, // Each node can handle 400/min
|
||||
currentCounts: { server1: 0, server2: 0, server3: 0 }
|
||||
};
|
||||
|
||||
console.log('\n\nSimulating distributed rate limiting:');
|
||||
console.log(`Global limit: ${distributedLimiter.globalLimit}/min`);
|
||||
console.log(`Per-node limit: ${distributedLimiter.perNodeLimit}/min`);
|
||||
|
||||
// Simulate load distribution
|
||||
for (let i = 0; i < 20; i++) {
|
||||
// Pick least loaded node
|
||||
const node = distributedLimiter.nodes.reduce((min, node) =>
|
||||
distributedLimiter.currentCounts[node] < distributedLimiter.currentCounts[min] ? node : min
|
||||
);
|
||||
|
||||
distributedLimiter.currentCounts[node]++;
|
||||
|
||||
if (i % 5 === 4) {
|
||||
console.log(`\nAfter ${i + 1} messages:`);
|
||||
distributedLimiter.nodes.forEach(n => {
|
||||
console.log(` ${n}: ${distributedLimiter.currentCounts[n]} messages`);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('CERR-08: Rate limit bypass strategies', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
console.log('\nLegitimate rate limit management strategies:');
|
||||
|
||||
// 1. Message batching
|
||||
console.log('\n1. Message batching:');
|
||||
const recipients = Array.from({ length: 50 }, (_, i) => `user${i}@example.com`);
|
||||
const batchSize = 10;
|
||||
|
||||
for (let i = 0; i < recipients.length; i += batchSize) {
|
||||
const batch = recipients.slice(i, i + batchSize);
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: batch,
|
||||
subject: 'Batched message',
|
||||
text: 'Sending in batches to respect rate limits'
|
||||
});
|
||||
|
||||
console.log(` Batch ${Math.floor(i/batchSize) + 1}: ${batch.length} recipients`);
|
||||
|
||||
try {
|
||||
await smtpClient.sendMail(email);
|
||||
|
||||
// Add delay between batches
|
||||
if (i + batchSize < recipients.length) {
|
||||
console.log(' Waiting 2 seconds before next batch...');
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(` Batch failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Connection pooling with limits
|
||||
console.log('\n2. Connection pooling:');
|
||||
console.log(' Using multiple connections with per-connection limits');
|
||||
console.log(' Example: 5 connections × 20 msg/min = 100 msg/min total');
|
||||
|
||||
// 3. Retry with backoff
|
||||
console.log('\n3. Exponential backoff on rate limits:');
|
||||
const backoffDelays = [1, 2, 4, 8, 16, 32];
|
||||
backoffDelays.forEach((delay, attempt) => {
|
||||
console.log(` Attempt ${attempt + 1}: Wait ${delay} seconds`);
|
||||
});
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('cleanup test SMTP server', async () => {
|
||||
if (testServer) {
|
||||
await testServer.stop();
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
@ -0,0 +1,616 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { startTestSmtpServer } from '../../helpers/server.loader.js';
|
||||
import { createSmtpClient } from '../../helpers/smtp.client.js';
|
||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
||||
import * as net from 'net';
|
||||
|
||||
let testServer: any;
|
||||
|
||||
tap.test('setup test SMTP server', async () => {
|
||||
testServer = await startTestSmtpServer();
|
||||
expect(testServer).toBeTruthy();
|
||||
expect(testServer.port).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('CERR-09: Pool exhaustion', async () => {
|
||||
const pooledClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 3,
|
||||
maxMessages: 100,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
console.log('Testing connection pool exhaustion...');
|
||||
console.log('Pool configuration: maxConnections=3');
|
||||
|
||||
// Track pool state
|
||||
const poolStats = {
|
||||
active: 0,
|
||||
idle: 0,
|
||||
pending: 0,
|
||||
created: 0,
|
||||
destroyed: 0
|
||||
};
|
||||
|
||||
pooledClient.on('pool-connection-create', () => {
|
||||
poolStats.created++;
|
||||
console.log(` Pool: Connection created (total: ${poolStats.created})`);
|
||||
});
|
||||
|
||||
pooledClient.on('pool-connection-close', () => {
|
||||
poolStats.destroyed++;
|
||||
console.log(` Pool: Connection closed (total: ${poolStats.destroyed})`);
|
||||
});
|
||||
|
||||
// Send more concurrent messages than pool size
|
||||
const messageCount = 10;
|
||||
const emails = Array.from({ length: messageCount }, (_, i) => new Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i}@example.com`],
|
||||
subject: `Pool test ${i}`,
|
||||
text: 'Testing connection pool exhaustion'
|
||||
}));
|
||||
|
||||
console.log(`\nSending ${messageCount} concurrent messages...`);
|
||||
|
||||
const startTime = Date.now();
|
||||
const results = await Promise.allSettled(
|
||||
emails.map((email, i) => {
|
||||
return pooledClient.sendMail(email).then(() => {
|
||||
console.log(` Message ${i}: Sent`);
|
||||
return { index: i, status: 'sent' };
|
||||
}).catch(error => {
|
||||
console.log(` Message ${i}: Failed - ${error.message}`);
|
||||
return { index: i, status: 'failed', error: error.message };
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const elapsed = Date.now() - startTime;
|
||||
|
||||
const successful = results.filter(r => r.status === 'fulfilled').length;
|
||||
const failed = results.filter(r => r.status === 'rejected').length;
|
||||
|
||||
console.log(`\nResults after ${elapsed}ms:`);
|
||||
console.log(` Successful: ${successful}/${messageCount}`);
|
||||
console.log(` Failed: ${failed}/${messageCount}`);
|
||||
console.log(` Connections created: ${poolStats.created}`);
|
||||
console.log(` Connections destroyed: ${poolStats.destroyed}`);
|
||||
|
||||
// Pool should limit concurrent connections
|
||||
expect(poolStats.created).toBeLessThanOrEqual(3);
|
||||
|
||||
await pooledClient.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-09: Connection pool timeouts', async () => {
|
||||
// Create slow server
|
||||
const slowServer = net.createServer((socket) => {
|
||||
socket.write('220 Slow Server\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
// Add delays to simulate slow responses
|
||||
setTimeout(() => {
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
} else {
|
||||
// Slow response for other commands
|
||||
setTimeout(() => {
|
||||
socket.write('250 OK\r\n');
|
||||
}, 3000); // 3 second delay
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
slowServer.listen(0, '127.0.0.1', () => resolve());
|
||||
});
|
||||
|
||||
const slowPort = (slowServer.address() as net.AddressInfo).port;
|
||||
|
||||
const pooledClient = createSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: slowPort,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 2,
|
||||
poolTimeout: 2000, // 2 second timeout for getting connection from pool
|
||||
commandTimeout: 4000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
console.log('\nTesting connection pool timeouts...');
|
||||
console.log('Pool timeout: 2 seconds');
|
||||
|
||||
// Send multiple messages to trigger pool timeout
|
||||
const emails = Array.from({ length: 5 }, (_, i) => new Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i}@example.com`],
|
||||
subject: `Timeout test ${i}`,
|
||||
text: 'Testing pool timeout'
|
||||
}));
|
||||
|
||||
const timeoutErrors = [];
|
||||
|
||||
await Promise.allSettled(
|
||||
emails.map(async (email, i) => {
|
||||
try {
|
||||
console.log(` Message ${i}: Attempting to send...`);
|
||||
await pooledClient.sendMail(email);
|
||||
console.log(` Message ${i}: Sent successfully`);
|
||||
} catch (error) {
|
||||
console.log(` Message ${i}: ${error.message}`);
|
||||
if (error.message.includes('timeout')) {
|
||||
timeoutErrors.push(error);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
console.log(`\nTimeout errors: ${timeoutErrors.length}`);
|
||||
expect(timeoutErrors.length).toBeGreaterThan(0);
|
||||
|
||||
await pooledClient.close();
|
||||
slowServer.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-09: Dead connection detection', async () => {
|
||||
const pooledClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 3,
|
||||
poolIdleTimeout: 5000, // Connections idle for 5s are closed
|
||||
poolPingInterval: 2000, // Ping idle connections every 2s
|
||||
debug: true
|
||||
});
|
||||
|
||||
console.log('\nTesting dead connection detection...');
|
||||
|
||||
// Track connection health checks
|
||||
let pingCount = 0;
|
||||
let deadConnections = 0;
|
||||
|
||||
pooledClient.on('pool-connection-ping', (result) => {
|
||||
pingCount++;
|
||||
console.log(` Ping ${pingCount}: ${result.alive ? 'Connection alive' : 'Connection dead'}`);
|
||||
if (!result.alive) {
|
||||
deadConnections++;
|
||||
}
|
||||
});
|
||||
|
||||
// Send initial message to create connection
|
||||
await pooledClient.sendMail(new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Initial message',
|
||||
text: 'Creating connection'
|
||||
}));
|
||||
|
||||
console.log('Connection created, waiting for health checks...');
|
||||
|
||||
// Wait for health checks
|
||||
await new Promise(resolve => setTimeout(resolve, 6000));
|
||||
|
||||
console.log(`\nHealth check results:`);
|
||||
console.log(` Total pings: ${pingCount}`);
|
||||
console.log(` Dead connections detected: ${deadConnections}`);
|
||||
|
||||
// Send another message to test connection recovery
|
||||
try {
|
||||
await pooledClient.sendMail(new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'After idle',
|
||||
text: 'Testing after idle period'
|
||||
}));
|
||||
console.log('Message sent successfully after idle period');
|
||||
} catch (error) {
|
||||
console.log('Error after idle:', error.message);
|
||||
}
|
||||
|
||||
await pooledClient.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-09: Pool connection limit per host', async () => {
|
||||
// Create multiple servers
|
||||
const servers = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const server = net.createServer((socket) => {
|
||||
socket.write(`220 Server ${i + 1}\r\n`);
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write(`250 server${i + 1}.example.com\r\n`);
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
server.listen(0, '127.0.0.1', () => resolve());
|
||||
});
|
||||
|
||||
servers.push({
|
||||
server,
|
||||
port: (server.address() as net.AddressInfo).port
|
||||
});
|
||||
}
|
||||
|
||||
console.log('\nTesting per-host connection limits...');
|
||||
|
||||
// Create pooled client with per-host limits
|
||||
const pooledClient = createSmtpClient({
|
||||
pool: true,
|
||||
maxConnections: 10, // Total pool size
|
||||
maxConnectionsPerHost: 2, // Per-host limit
|
||||
debug: true
|
||||
});
|
||||
|
||||
// Track connections per host
|
||||
const hostConnections: { [key: string]: number } = {};
|
||||
|
||||
pooledClient.on('pool-connection-create', (info) => {
|
||||
const host = info.host || 'unknown';
|
||||
hostConnections[host] = (hostConnections[host] || 0) + 1;
|
||||
console.log(` Created connection to ${host} (total: ${hostConnections[host]})`);
|
||||
});
|
||||
|
||||
// Send messages to different servers
|
||||
const messages = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
for (let j = 0; j < 4; j++) {
|
||||
messages.push({
|
||||
server: i,
|
||||
email: new Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${j}@server${i}.com`],
|
||||
subject: `Test ${j} to server ${i}`,
|
||||
text: 'Testing per-host limits'
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Override host/port for each message
|
||||
await Promise.allSettled(
|
||||
messages.map(async ({ server, email }) => {
|
||||
const client = createSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: servers[server].port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 10,
|
||||
maxConnectionsPerHost: 2,
|
||||
debug: false
|
||||
});
|
||||
|
||||
try {
|
||||
await client.sendMail(email);
|
||||
console.log(` Sent to server ${server + 1}`);
|
||||
} catch (error) {
|
||||
console.log(` Failed to server ${server + 1}: ${error.message}`);
|
||||
}
|
||||
|
||||
await client.close();
|
||||
})
|
||||
);
|
||||
|
||||
console.log('\nConnections per host:');
|
||||
Object.entries(hostConnections).forEach(([host, count]) => {
|
||||
console.log(` ${host}: ${count} connections`);
|
||||
expect(count).toBeLessThanOrEqual(2); // Should respect per-host limit
|
||||
});
|
||||
|
||||
// Clean up servers
|
||||
servers.forEach(s => s.server.close());
|
||||
});
|
||||
|
||||
tap.test('CERR-09: Connection pool recovery', async () => {
|
||||
// Create unstable server
|
||||
let shouldFail = true;
|
||||
let requestCount = 0;
|
||||
|
||||
const unstableServer = net.createServer((socket) => {
|
||||
requestCount++;
|
||||
|
||||
if (shouldFail && requestCount <= 3) {
|
||||
// Abruptly close connection for first 3 requests
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
socket.write('220 Unstable Server\r\n');
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
unstableServer.listen(0, '127.0.0.1', () => resolve());
|
||||
});
|
||||
|
||||
const unstablePort = (unstableServer.address() as net.AddressInfo).port;
|
||||
|
||||
const pooledClient = createSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: unstablePort,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 2,
|
||||
retryFailedConnections: true,
|
||||
connectionRetryDelay: 1000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
console.log('\nTesting connection pool recovery...');
|
||||
console.log('Server will fail first 3 connection attempts');
|
||||
|
||||
// Track recovery attempts
|
||||
let recoveryAttempts = 0;
|
||||
pooledClient.on('pool-connection-retry', () => {
|
||||
recoveryAttempts++;
|
||||
console.log(` Recovery attempt ${recoveryAttempts}`);
|
||||
});
|
||||
|
||||
// Try to send messages
|
||||
const results = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i}@example.com`],
|
||||
subject: `Recovery test ${i}`,
|
||||
text: 'Testing connection recovery'
|
||||
});
|
||||
|
||||
try {
|
||||
console.log(`\nMessage ${i}: Attempting...`);
|
||||
await pooledClient.sendMail(email);
|
||||
console.log(`Message ${i}: Success`);
|
||||
results.push('success');
|
||||
} catch (error) {
|
||||
console.log(`Message ${i}: Failed - ${error.message}`);
|
||||
results.push('failed');
|
||||
|
||||
// After some failures, allow connections
|
||||
if (i === 2) {
|
||||
shouldFail = false;
|
||||
console.log(' Server stabilized, connections should succeed now');
|
||||
}
|
||||
}
|
||||
|
||||
// Small delay between attempts
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
console.log('\nFinal results:', results);
|
||||
const successCount = results.filter(r => r === 'success').length;
|
||||
expect(successCount).toBeGreaterThan(0); // Should recover eventually
|
||||
|
||||
await pooledClient.close();
|
||||
unstableServer.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-09: Pool metrics and monitoring', async () => {
|
||||
const pooledClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 5,
|
||||
poolMetrics: true,
|
||||
debug: true
|
||||
});
|
||||
|
||||
console.log('\nTesting pool metrics collection...');
|
||||
|
||||
// Collect metrics
|
||||
const metrics = {
|
||||
connectionsCreated: 0,
|
||||
connectionsDestroyed: 0,
|
||||
messagesQueued: 0,
|
||||
messagesSent: 0,
|
||||
errors: 0,
|
||||
avgWaitTime: 0,
|
||||
waitTimes: [] as number[]
|
||||
};
|
||||
|
||||
pooledClient.on('pool-metrics', (data) => {
|
||||
Object.assign(metrics, data);
|
||||
});
|
||||
|
||||
pooledClient.on('message-queued', () => {
|
||||
metrics.messagesQueued++;
|
||||
});
|
||||
|
||||
pooledClient.on('message-sent', (info) => {
|
||||
metrics.messagesSent++;
|
||||
if (info.waitTime) {
|
||||
metrics.waitTimes.push(info.waitTime);
|
||||
}
|
||||
});
|
||||
|
||||
// Send batch of messages
|
||||
const messageCount = 20;
|
||||
const startTime = Date.now();
|
||||
|
||||
await Promise.allSettled(
|
||||
Array.from({ length: messageCount }, (_, i) =>
|
||||
pooledClient.sendMail(new Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i}@example.com`],
|
||||
subject: `Metrics test ${i}`,
|
||||
text: 'Testing pool metrics'
|
||||
}))
|
||||
)
|
||||
);
|
||||
|
||||
const totalTime = Date.now() - startTime;
|
||||
|
||||
// Calculate average wait time
|
||||
if (metrics.waitTimes.length > 0) {
|
||||
metrics.avgWaitTime = metrics.waitTimes.reduce((a, b) => a + b, 0) / metrics.waitTimes.length;
|
||||
}
|
||||
|
||||
// Get final pool status
|
||||
const poolStatus = pooledClient.getPoolStatus();
|
||||
|
||||
console.log('\nPool Metrics:');
|
||||
console.log(` Messages queued: ${metrics.messagesQueued}`);
|
||||
console.log(` Messages sent: ${metrics.messagesSent}`);
|
||||
console.log(` Average wait time: ${metrics.avgWaitTime.toFixed(2)}ms`);
|
||||
console.log(` Total time: ${totalTime}ms`);
|
||||
console.log(` Throughput: ${(messageCount / totalTime * 1000).toFixed(2)} msg/sec`);
|
||||
console.log('\nPool Status:');
|
||||
console.log(` Active connections: ${poolStatus.active}`);
|
||||
console.log(` Idle connections: ${poolStatus.idle}`);
|
||||
console.log(` Total connections: ${poolStatus.total}`);
|
||||
|
||||
await pooledClient.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-09: Connection affinity', async () => {
|
||||
const pooledClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 3,
|
||||
connectionAffinity: 'sender', // Reuse same connection for same sender
|
||||
debug: true
|
||||
});
|
||||
|
||||
console.log('\nTesting connection affinity...');
|
||||
|
||||
// Track which connection handles which sender
|
||||
const senderConnections: { [sender: string]: string } = {};
|
||||
|
||||
pooledClient.on('connection-assigned', (info) => {
|
||||
senderConnections[info.sender] = info.connectionId;
|
||||
console.log(` Sender ${info.sender} assigned to connection ${info.connectionId}`);
|
||||
});
|
||||
|
||||
// Send messages from different senders
|
||||
const senders = ['alice@example.com', 'bob@example.com', 'alice@example.com', 'charlie@example.com', 'bob@example.com'];
|
||||
|
||||
for (const sender of senders) {
|
||||
const email = new Email({
|
||||
from: sender,
|
||||
to: ['recipient@example.com'],
|
||||
subject: `From ${sender}`,
|
||||
text: 'Testing connection affinity'
|
||||
});
|
||||
|
||||
await pooledClient.sendMail(email);
|
||||
|
||||
const connectionId = senderConnections[sender];
|
||||
console.log(` Message from ${sender} sent via connection ${connectionId}`);
|
||||
}
|
||||
|
||||
// Verify affinity
|
||||
console.log('\nConnection affinity results:');
|
||||
const uniqueSenders = [...new Set(senders)];
|
||||
uniqueSenders.forEach(sender => {
|
||||
const messages = senders.filter(s => s === sender).length;
|
||||
console.log(` ${sender}: ${messages} messages, connection ${senderConnections[sender]}`);
|
||||
});
|
||||
|
||||
await pooledClient.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-09: Pool resource cleanup', async () => {
|
||||
const pooledClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 3,
|
||||
poolCleanupInterval: 1000, // Clean up every second
|
||||
debug: true
|
||||
});
|
||||
|
||||
console.log('\nTesting pool resource cleanup...');
|
||||
|
||||
// Track cleanup events
|
||||
const cleanupStats = {
|
||||
idleClosed: 0,
|
||||
staleClosed: 0,
|
||||
errorClosed: 0
|
||||
};
|
||||
|
||||
pooledClient.on('pool-connection-cleanup', (reason) => {
|
||||
switch (reason.type) {
|
||||
case 'idle':
|
||||
cleanupStats.idleClosed++;
|
||||
console.log(` Closed idle connection: ${reason.connectionId}`);
|
||||
break;
|
||||
case 'stale':
|
||||
cleanupStats.staleClosed++;
|
||||
console.log(` Closed stale connection: ${reason.connectionId}`);
|
||||
break;
|
||||
case 'error':
|
||||
cleanupStats.errorClosed++;
|
||||
console.log(` Closed errored connection: ${reason.connectionId}`);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Send some messages
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await pooledClient.sendMail(new Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i}@example.com`],
|
||||
subject: `Cleanup test ${i}`,
|
||||
text: 'Testing cleanup'
|
||||
}));
|
||||
}
|
||||
|
||||
console.log('Messages sent, waiting for cleanup...');
|
||||
|
||||
// Wait for cleanup cycles
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
|
||||
console.log('\nCleanup statistics:');
|
||||
console.log(` Idle connections closed: ${cleanupStats.idleClosed}`);
|
||||
console.log(` Stale connections closed: ${cleanupStats.staleClosed}`);
|
||||
console.log(` Errored connections closed: ${cleanupStats.errorClosed}`);
|
||||
|
||||
const finalStatus = pooledClient.getPoolStatus();
|
||||
console.log(`\nFinal pool status:`);
|
||||
console.log(` Active: ${finalStatus.active}`);
|
||||
console.log(` Idle: ${finalStatus.idle}`);
|
||||
console.log(` Total: ${finalStatus.total}`);
|
||||
|
||||
await pooledClient.close();
|
||||
});
|
||||
|
||||
tap.test('cleanup test SMTP server', async () => {
|
||||
if (testServer) {
|
||||
await testServer.stop();
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
@ -0,0 +1,628 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { startTestSmtpServer } from '../../helpers/server.loader.js';
|
||||
import { createSmtpClient } from '../../helpers/smtp.client.js';
|
||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
||||
import * as net from 'net';
|
||||
|
||||
let testServer: any;
|
||||
|
||||
tap.test('setup test SMTP server', async () => {
|
||||
testServer = await startTestSmtpServer();
|
||||
expect(testServer).toBeTruthy();
|
||||
expect(testServer.port).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('CERR-10: Partial recipient failure', async () => {
|
||||
// Create server that accepts some recipients and rejects others
|
||||
const partialFailureServer = net.createServer((socket) => {
|
||||
socket.write('220 Partial Failure Test Server\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO')) {
|
||||
const recipient = command.match(/<([^>]+)>/)?.[1] || '';
|
||||
|
||||
// Accept/reject based on recipient
|
||||
if (recipient.includes('valid')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (recipient.includes('invalid')) {
|
||||
socket.write('550 5.1.1 User unknown\r\n');
|
||||
} else if (recipient.includes('full')) {
|
||||
socket.write('452 4.2.2 Mailbox full\r\n');
|
||||
} else if (recipient.includes('greylisted')) {
|
||||
socket.write('451 4.7.1 Greylisted, try again later\r\n');
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Send data\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK - delivered to accepted recipients only\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
partialFailureServer.listen(0, '127.0.0.1', () => resolve());
|
||||
});
|
||||
|
||||
const partialPort = (partialFailureServer.address() as net.AddressInfo).port;
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: partialPort,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
continueOnRecipientError: true, // Continue even if some recipients fail
|
||||
debug: true
|
||||
});
|
||||
|
||||
console.log('Testing partial recipient failure...');
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: [
|
||||
'valid1@example.com',
|
||||
'invalid@example.com',
|
||||
'valid2@example.com',
|
||||
'full@example.com',
|
||||
'valid3@example.com',
|
||||
'greylisted@example.com'
|
||||
],
|
||||
subject: 'Partial failure test',
|
||||
text: 'Testing partial recipient failures'
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await smtpClient.sendMail(email);
|
||||
|
||||
console.log('\nPartial send results:');
|
||||
console.log(` Total recipients: ${email.to.length}`);
|
||||
console.log(` Accepted: ${result.accepted?.length || 0}`);
|
||||
console.log(` Rejected: ${result.rejected?.length || 0}`);
|
||||
console.log(` Pending: ${result.pending?.length || 0}`);
|
||||
|
||||
if (result.accepted && result.accepted.length > 0) {
|
||||
console.log('\nAccepted recipients:');
|
||||
result.accepted.forEach(r => console.log(` ✓ ${r}`));
|
||||
}
|
||||
|
||||
if (result.rejected && result.rejected.length > 0) {
|
||||
console.log('\nRejected recipients:');
|
||||
result.rejected.forEach(r => console.log(` ✗ ${r.recipient}: ${r.reason}`));
|
||||
}
|
||||
|
||||
if (result.pending && result.pending.length > 0) {
|
||||
console.log('\nPending recipients (temporary failures):');
|
||||
result.pending.forEach(r => console.log(` ⏳ ${r.recipient}: ${r.reason}`));
|
||||
}
|
||||
|
||||
// Should have partial success
|
||||
expect(result.accepted?.length).toBeGreaterThan(0);
|
||||
expect(result.rejected?.length).toBeGreaterThan(0);
|
||||
|
||||
} catch (error) {
|
||||
console.log('Unexpected complete failure:', error.message);
|
||||
}
|
||||
|
||||
await smtpClient.close();
|
||||
partialFailureServer.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-10: Partial failure policies', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
console.log('\nTesting different partial failure policies:');
|
||||
|
||||
// Policy configurations
|
||||
const policies = [
|
||||
{
|
||||
name: 'Fail if any recipient fails',
|
||||
continueOnError: false,
|
||||
minSuccessRate: 1.0
|
||||
},
|
||||
{
|
||||
name: 'Continue if any recipient succeeds',
|
||||
continueOnError: true,
|
||||
minSuccessRate: 0.01
|
||||
},
|
||||
{
|
||||
name: 'Require 50% success rate',
|
||||
continueOnError: true,
|
||||
minSuccessRate: 0.5
|
||||
},
|
||||
{
|
||||
name: 'Require at least 2 recipients',
|
||||
continueOnError: true,
|
||||
minSuccessCount: 2
|
||||
}
|
||||
];
|
||||
|
||||
for (const policy of policies) {
|
||||
console.log(`\n${policy.name}:`);
|
||||
console.log(` Continue on error: ${policy.continueOnError}`);
|
||||
if (policy.minSuccessRate !== undefined) {
|
||||
console.log(` Min success rate: ${(policy.minSuccessRate * 100).toFixed(0)}%`);
|
||||
}
|
||||
if (policy.minSuccessCount !== undefined) {
|
||||
console.log(` Min success count: ${policy.minSuccessCount}`);
|
||||
}
|
||||
|
||||
// Simulate applying policy
|
||||
const results = {
|
||||
accepted: ['user1@example.com', 'user2@example.com'],
|
||||
rejected: ['invalid@example.com'],
|
||||
total: 3
|
||||
};
|
||||
|
||||
const successRate = results.accepted.length / results.total;
|
||||
let shouldProceed = policy.continueOnError;
|
||||
|
||||
if (policy.minSuccessRate !== undefined) {
|
||||
shouldProceed = shouldProceed && (successRate >= policy.minSuccessRate);
|
||||
}
|
||||
|
||||
if (policy.minSuccessCount !== undefined) {
|
||||
shouldProceed = shouldProceed && (results.accepted.length >= policy.minSuccessCount);
|
||||
}
|
||||
|
||||
console.log(` With ${results.accepted.length}/${results.total} success: ${shouldProceed ? 'PROCEED' : 'FAIL'}`);
|
||||
}
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-10: Partial data transmission failure', async () => {
|
||||
// Server that fails during DATA phase
|
||||
const dataFailureServer = net.createServer((socket) => {
|
||||
let dataSize = 0;
|
||||
let inData = false;
|
||||
|
||||
socket.write('220 Data Failure Test Server\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString();
|
||||
|
||||
if (command.trim().startsWith('EHLO')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.trim().startsWith('MAIL FROM')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.trim().startsWith('RCPT TO')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.trim() === 'DATA') {
|
||||
inData = true;
|
||||
dataSize = 0;
|
||||
socket.write('354 Send data\r\n');
|
||||
} else if (inData) {
|
||||
dataSize += data.length;
|
||||
|
||||
// Fail after receiving 1KB of data
|
||||
if (dataSize > 1024) {
|
||||
socket.write('451 4.3.0 Message transmission failed\r\n');
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
if (command.includes('\r\n.\r\n')) {
|
||||
inData = false;
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else if (command.trim() === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
dataFailureServer.listen(0, '127.0.0.1', () => resolve());
|
||||
});
|
||||
|
||||
const dataFailurePort = (dataFailureServer.address() as net.AddressInfo).port;
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: dataFailurePort,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
console.log('\nTesting partial data transmission failure...');
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
// Try to send large message that will fail during transmission
|
||||
const largeEmail = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Large message test',
|
||||
text: 'x'.repeat(2048) // 2KB - will fail after 1KB
|
||||
});
|
||||
|
||||
try {
|
||||
await smtpClient.sendMail(largeEmail);
|
||||
console.log('Unexpected success');
|
||||
} catch (error) {
|
||||
console.log('Data transmission failed as expected:', error.message);
|
||||
expect(error.message).toMatch(/451|transmission|failed/i);
|
||||
}
|
||||
|
||||
// Try smaller message that should succeed
|
||||
const smallEmail = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Small message test',
|
||||
text: 'This is a small message'
|
||||
});
|
||||
|
||||
// Need new connection after failure
|
||||
await smtpClient.close();
|
||||
await smtpClient.connect();
|
||||
|
||||
try {
|
||||
await smtpClient.sendMail(smallEmail);
|
||||
console.log('Small message sent successfully');
|
||||
} catch (error) {
|
||||
console.log('Small message also failed:', error.message);
|
||||
}
|
||||
|
||||
await smtpClient.close();
|
||||
dataFailureServer.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-10: Partial failure recovery strategies', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
partialFailureStrategy: 'retry-failed',
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
console.log('\nPartial failure recovery strategies:');
|
||||
|
||||
const strategies = [
|
||||
{
|
||||
name: 'Retry failed recipients',
|
||||
description: 'Queue failed recipients for retry',
|
||||
implementation: async (result: any) => {
|
||||
if (result.rejected && result.rejected.length > 0) {
|
||||
console.log(` Queueing ${result.rejected.length} recipients for retry`);
|
||||
// Would implement retry queue here
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Bounce failed recipients',
|
||||
description: 'Send bounce notifications immediately',
|
||||
implementation: async (result: any) => {
|
||||
if (result.rejected && result.rejected.length > 0) {
|
||||
console.log(` Generating bounce messages for ${result.rejected.length} recipients`);
|
||||
// Would generate NDR here
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Split and retry',
|
||||
description: 'Split into individual messages',
|
||||
implementation: async (result: any) => {
|
||||
if (result.rejected && result.rejected.length > 0) {
|
||||
console.log(` Splitting into ${result.rejected.length} individual messages`);
|
||||
// Would send individual messages here
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Fallback transport',
|
||||
description: 'Try alternative delivery method',
|
||||
implementation: async (result: any) => {
|
||||
if (result.rejected && result.rejected.length > 0) {
|
||||
console.log(` Attempting fallback delivery for ${result.rejected.length} recipients`);
|
||||
// Would try alternative server/route here
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// Simulate partial failure
|
||||
const mockResult = {
|
||||
accepted: ['user1@example.com', 'user2@example.com'],
|
||||
rejected: [
|
||||
{ recipient: 'invalid@example.com', reason: '550 User unknown' },
|
||||
{ recipient: 'full@example.com', reason: '552 Mailbox full' }
|
||||
],
|
||||
pending: [
|
||||
{ recipient: 'greylisted@example.com', reason: '451 Greylisted' }
|
||||
]
|
||||
};
|
||||
|
||||
for (const strategy of strategies) {
|
||||
console.log(`\n${strategy.name}:`);
|
||||
console.log(` Description: ${strategy.description}`);
|
||||
await strategy.implementation(mockResult);
|
||||
}
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-10: Transaction state after partial failure', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
console.log('\nTesting transaction state after partial failure...');
|
||||
|
||||
// Start transaction
|
||||
await smtpClient.sendCommand('MAIL FROM:<sender@example.com>');
|
||||
|
||||
// Add recipients with mixed results
|
||||
const recipients = [
|
||||
{ email: 'valid@example.com', shouldSucceed: true },
|
||||
{ email: 'invalid@nonexistent.com', shouldSucceed: false },
|
||||
{ email: 'another@example.com', shouldSucceed: true }
|
||||
];
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const recipient of recipients) {
|
||||
try {
|
||||
const response = await smtpClient.sendCommand(`RCPT TO:<${recipient.email}>`);
|
||||
results.push({
|
||||
email: recipient.email,
|
||||
success: response.startsWith('250'),
|
||||
response: response.trim()
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
email: recipient.email,
|
||||
success: false,
|
||||
response: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\nRecipient results:');
|
||||
results.forEach(r => {
|
||||
console.log(` ${r.email}: ${r.success ? '✓' : '✗'} ${r.response}`);
|
||||
});
|
||||
|
||||
const acceptedCount = results.filter(r => r.success).length;
|
||||
|
||||
if (acceptedCount > 0) {
|
||||
console.log(`\n${acceptedCount} recipients accepted, proceeding with DATA...`);
|
||||
|
||||
try {
|
||||
const dataResponse = await smtpClient.sendCommand('DATA');
|
||||
console.log('DATA response:', dataResponse.trim());
|
||||
|
||||
if (dataResponse.startsWith('354')) {
|
||||
await smtpClient.sendCommand('Subject: Partial recipient test\r\n\r\nTest message\r\n.');
|
||||
console.log('Message sent to accepted recipients');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('DATA phase error:', error.message);
|
||||
}
|
||||
} else {
|
||||
console.log('\nNo recipients accepted, resetting transaction');
|
||||
await smtpClient.sendCommand('RSET');
|
||||
}
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-10: Partial authentication failure', async () => {
|
||||
// Server with selective authentication
|
||||
const authFailureServer = net.createServer((socket) => {
|
||||
socket.write('220 Auth Failure Test Server\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-authfailure.example.com\r\n');
|
||||
socket.write('250-AUTH PLAIN LOGIN\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('AUTH')) {
|
||||
// Randomly fail authentication
|
||||
if (Math.random() > 0.5) {
|
||||
socket.write('235 2.7.0 Authentication successful\r\n');
|
||||
} else {
|
||||
socket.write('535 5.7.8 Authentication credentials invalid\r\n');
|
||||
}
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
authFailureServer.listen(0, '127.0.0.1', () => resolve());
|
||||
});
|
||||
|
||||
const authPort = (authFailureServer.address() as net.AddressInfo).port;
|
||||
|
||||
console.log('\nTesting partial authentication failure with fallback...');
|
||||
|
||||
// Try multiple authentication methods
|
||||
const authMethods = [
|
||||
{ method: 'PLAIN', credentials: 'user1:pass1' },
|
||||
{ method: 'LOGIN', credentials: 'user2:pass2' },
|
||||
{ method: 'PLAIN', credentials: 'user3:pass3' }
|
||||
];
|
||||
|
||||
let authenticated = false;
|
||||
let attempts = 0;
|
||||
|
||||
for (const auth of authMethods) {
|
||||
attempts++;
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: authPort,
|
||||
secure: false,
|
||||
auth: {
|
||||
method: auth.method,
|
||||
user: auth.credentials.split(':')[0],
|
||||
pass: auth.credentials.split(':')[1]
|
||||
},
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
console.log(`\nAttempt ${attempts}: ${auth.method} authentication`);
|
||||
|
||||
try {
|
||||
await smtpClient.connect();
|
||||
authenticated = true;
|
||||
console.log('Authentication successful');
|
||||
|
||||
// Send test message
|
||||
await smtpClient.sendMail(new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Auth test',
|
||||
text: 'Successfully authenticated'
|
||||
}));
|
||||
|
||||
await smtpClient.close();
|
||||
break;
|
||||
} catch (error) {
|
||||
console.log('Authentication failed:', error.message);
|
||||
await smtpClient.close();
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nAuthentication ${authenticated ? 'succeeded' : 'failed'} after ${attempts} attempts`);
|
||||
|
||||
authFailureServer.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-10: Partial failure reporting', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
generatePartialFailureReport: true,
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
console.log('\nGenerating partial failure report...');
|
||||
|
||||
// Simulate partial failure result
|
||||
const partialResult = {
|
||||
messageId: '<123456@example.com>',
|
||||
timestamp: new Date(),
|
||||
from: 'sender@example.com',
|
||||
accepted: [
|
||||
'user1@example.com',
|
||||
'user2@example.com',
|
||||
'user3@example.com'
|
||||
],
|
||||
rejected: [
|
||||
{ recipient: 'invalid@example.com', code: '550', reason: 'User unknown' },
|
||||
{ recipient: 'full@example.com', code: '552', reason: 'Mailbox full' }
|
||||
],
|
||||
pending: [
|
||||
{ recipient: 'grey@example.com', code: '451', reason: 'Greylisted' }
|
||||
]
|
||||
};
|
||||
|
||||
// Generate failure report
|
||||
const report = {
|
||||
summary: {
|
||||
total: partialResult.accepted.length + partialResult.rejected.length + partialResult.pending.length,
|
||||
delivered: partialResult.accepted.length,
|
||||
failed: partialResult.rejected.length,
|
||||
deferred: partialResult.pending.length,
|
||||
successRate: ((partialResult.accepted.length / (partialResult.accepted.length + partialResult.rejected.length + partialResult.pending.length)) * 100).toFixed(1)
|
||||
},
|
||||
details: {
|
||||
messageId: partialResult.messageId,
|
||||
timestamp: partialResult.timestamp.toISOString(),
|
||||
from: partialResult.from,
|
||||
recipients: {
|
||||
delivered: partialResult.accepted,
|
||||
failed: partialResult.rejected.map(r => ({
|
||||
address: r.recipient,
|
||||
error: `${r.code} ${r.reason}`,
|
||||
permanent: r.code.startsWith('5')
|
||||
})),
|
||||
deferred: partialResult.pending.map(r => ({
|
||||
address: r.recipient,
|
||||
error: `${r.code} ${r.reason}`,
|
||||
retryAfter: new Date(Date.now() + 300000).toISOString() // 5 minutes
|
||||
}))
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
failed: 'Generate bounce notifications',
|
||||
deferred: 'Queue for retry in 5 minutes'
|
||||
}
|
||||
};
|
||||
|
||||
console.log('\nPartial Failure Report:');
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
|
||||
// Send notification email about partial failure
|
||||
const notificationEmail = new Email({
|
||||
from: 'postmaster@example.com',
|
||||
to: ['sender@example.com'],
|
||||
subject: 'Partial delivery failure',
|
||||
text: `Your message ${partialResult.messageId} was partially delivered.\n\n` +
|
||||
`Delivered: ${report.summary.delivered}\n` +
|
||||
`Failed: ${report.summary.failed}\n` +
|
||||
`Deferred: ${report.summary.deferred}\n` +
|
||||
`Success rate: ${report.summary.successRate}%`
|
||||
});
|
||||
|
||||
try {
|
||||
await smtpClient.sendMail(notificationEmail);
|
||||
console.log('\nPartial failure notification sent');
|
||||
} catch (error) {
|
||||
console.log('Failed to send notification:', error.message);
|
||||
}
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('cleanup test SMTP server', async () => {
|
||||
if (testServer) {
|
||||
await testServer.stop();
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
@ -0,0 +1,548 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as plugins from './plugins.js';
|
||||
import { createTestServer } from '../../helpers/server.loader.js';
|
||||
import { createSmtpClient } from '../../helpers/smtp.client.js';
|
||||
|
||||
tap.test('CRFC-02: should comply with ESMTP extensions (RFC 1869)', async (tools) => {
|
||||
const testId = 'CRFC-02-esmtp-compliance';
|
||||
console.log(`\n${testId}: Testing ESMTP extension compliance...`);
|
||||
|
||||
let scenarioCount = 0;
|
||||
|
||||
// Scenario 1: EHLO vs HELO negotiation
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing EHLO vs HELO negotiation`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 esmtp.example.com ESMTP Service Ready\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
// ESMTP response with extensions
|
||||
socket.write('250-esmtp.example.com Hello\r\n');
|
||||
socket.write('250-SIZE 35882577\r\n');
|
||||
socket.write('250-8BITMIME\r\n');
|
||||
socket.write('250-STARTTLS\r\n');
|
||||
socket.write('250-ENHANCEDSTATUSCODES\r\n');
|
||||
socket.write('250-PIPELINING\r\n');
|
||||
socket.write('250-CHUNKING\r\n');
|
||||
socket.write('250-SMTPUTF8\r\n');
|
||||
socket.write('250 DSN\r\n');
|
||||
} else if (command.startsWith('HELO')) {
|
||||
// Basic SMTP response
|
||||
socket.write('250 esmtp.example.com\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
// Check for ESMTP parameters
|
||||
if (command.includes('SIZE=')) {
|
||||
console.log(' [Server] SIZE parameter detected');
|
||||
}
|
||||
if (command.includes('BODY=')) {
|
||||
console.log(' [Server] BODY parameter detected');
|
||||
}
|
||||
socket.write('250 2.1.0 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
// Check for DSN parameters
|
||||
if (command.includes('NOTIFY=')) {
|
||||
console.log(' [Server] NOTIFY parameter detected');
|
||||
}
|
||||
if (command.includes('ORCPT=')) {
|
||||
console.log(' [Server] ORCPT parameter detected');
|
||||
}
|
||||
socket.write('250 2.1.5 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 2.0.0 OK: Message accepted\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 2.0.0 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Test EHLO with ESMTP client
|
||||
const esmtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
name: 'client.example.com'
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'ESMTP test',
|
||||
text: 'Testing ESMTP negotiation'
|
||||
});
|
||||
|
||||
const result = await esmtpClient.sendMail(email);
|
||||
console.log(' EHLO negotiation successful');
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
|
||||
// Test fallback to HELO
|
||||
const basicClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
name: 'basic.example.com',
|
||||
disableESMTP: true // Force HELO
|
||||
});
|
||||
|
||||
const heloResult = await basicClient.sendMail(email);
|
||||
console.log(' HELO fallback successful');
|
||||
expect(heloResult).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 2: SIZE extension compliance
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing SIZE extension compliance`);
|
||||
|
||||
const maxSize = 5242880; // 5MB
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 size.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-size.example.com\r\n');
|
||||
socket.write(`250-SIZE ${maxSize}\r\n`);
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
// Extract SIZE parameter
|
||||
const sizeMatch = command.match(/SIZE=(\d+)/i);
|
||||
if (sizeMatch) {
|
||||
const declaredSize = parseInt(sizeMatch[1]);
|
||||
console.log(` [Server] Client declared size: ${declaredSize}`);
|
||||
|
||||
if (declaredSize > maxSize) {
|
||||
socket.write(`552 5.3.4 Message size exceeds fixed maximum message size (${maxSize})\r\n`);
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Test 1: Small message
|
||||
const smallEmail = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Small message',
|
||||
text: 'This is a small message'
|
||||
});
|
||||
|
||||
const smallResult = await smtpClient.sendMail(smallEmail);
|
||||
console.log(' Small message accepted');
|
||||
expect(smallResult).toBeDefined();
|
||||
|
||||
// Test 2: Large message
|
||||
const largeEmail = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Large message',
|
||||
text: 'X'.repeat(maxSize + 1000) // Exceed limit
|
||||
});
|
||||
|
||||
try {
|
||||
await smtpClient.sendMail(largeEmail);
|
||||
console.log(' Unexpected: Large message accepted');
|
||||
} catch (error) {
|
||||
console.log(' Large message rejected as expected');
|
||||
expect(error.message).toContain('size exceeds');
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 3: 8BITMIME extension
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing 8BITMIME extension`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 8bit.example.com ESMTP\r\n');
|
||||
|
||||
let bodyType = '7BIT';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-8bit.example.com\r\n');
|
||||
socket.write('250-8BITMIME\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
// Check BODY parameter
|
||||
const bodyMatch = command.match(/BODY=(\w+)/i);
|
||||
if (bodyMatch) {
|
||||
bodyType = bodyMatch[1].toUpperCase();
|
||||
console.log(` [Server] BODY type: ${bodyType}`);
|
||||
|
||||
if (bodyType === '8BITMIME' || bodyType === '7BIT') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else {
|
||||
socket.write('555 5.5.4 Unsupported BODY type\r\n');
|
||||
}
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write(`250 OK: Message accepted (BODY=${bodyType})\r\n`);
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Test with 8-bit content
|
||||
const email8bit = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Testing 8BITMIME with UTF-8: café, naïve, 你好',
|
||||
text: 'Message with 8-bit characters: émojis 🎉, spéçiål çhåracters, 日本語',
|
||||
encoding: '8bit'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email8bit);
|
||||
console.log(' 8BITMIME message accepted');
|
||||
expect(result).toBeDefined();
|
||||
expect(result.response).toContain('BODY=8BITMIME');
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 4: PIPELINING extension
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing PIPELINING extension`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 pipeline.example.com ESMTP\r\n');
|
||||
|
||||
let commandBuffer: string[] = [];
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const commands = data.toString().split('\r\n').filter(cmd => cmd.length > 0);
|
||||
|
||||
// With pipelining, multiple commands can arrive at once
|
||||
if (commands.length > 1) {
|
||||
console.log(` [Server] Received ${commands.length} pipelined commands`);
|
||||
}
|
||||
|
||||
commands.forEach(command => {
|
||||
console.log(` [Server] Processing: ${command}`);
|
||||
commandBuffer.push(command);
|
||||
});
|
||||
|
||||
// Process buffered commands
|
||||
while (commandBuffer.length > 0) {
|
||||
const command = commandBuffer.shift()!;
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-pipeline.example.com\r\n');
|
||||
socket.write('250-PIPELINING\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK: Pipelined message accepted\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pipelining: true
|
||||
});
|
||||
|
||||
// Send email with multiple recipients (tests pipelining)
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient1@example.com', 'recipient2@example.com', 'recipient3@example.com'],
|
||||
subject: 'Pipelining test',
|
||||
text: 'Testing SMTP command pipelining'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(' Pipelined commands successful');
|
||||
expect(result).toBeDefined();
|
||||
expect(result.response).toContain('Pipelined');
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 5: DSN (Delivery Status Notification) extension
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing DSN extension`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 dsn.example.com ESMTP\r\n');
|
||||
|
||||
let envid = '';
|
||||
const recipients: Array<{ address: string; notify: string; orcpt: string }> = [];
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-dsn.example.com\r\n');
|
||||
socket.write('250-DSN\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
// Check for ENVID parameter
|
||||
const envidMatch = command.match(/ENVID=([^\s]+)/i);
|
||||
if (envidMatch) {
|
||||
envid = envidMatch[1];
|
||||
console.log(` [Server] ENVID: ${envid}`);
|
||||
}
|
||||
|
||||
// Check for RET parameter
|
||||
const retMatch = command.match(/RET=(FULL|HDRS)/i);
|
||||
if (retMatch) {
|
||||
console.log(` [Server] RET: ${retMatch[1]}`);
|
||||
}
|
||||
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
const address = command.match(/<(.+)>/)?.[1] || '';
|
||||
let notify = 'NEVER';
|
||||
let orcpt = '';
|
||||
|
||||
// Check NOTIFY parameter
|
||||
const notifyMatch = command.match(/NOTIFY=([^\s]+)/i);
|
||||
if (notifyMatch) {
|
||||
notify = notifyMatch[1];
|
||||
console.log(` [Server] NOTIFY for ${address}: ${notify}`);
|
||||
}
|
||||
|
||||
// Check ORCPT parameter
|
||||
const orcptMatch = command.match(/ORCPT=([^\s]+)/i);
|
||||
if (orcptMatch) {
|
||||
orcpt = orcptMatch[1];
|
||||
console.log(` [Server] ORCPT: ${orcpt}`);
|
||||
}
|
||||
|
||||
recipients.push({ address, notify, orcpt });
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
const dsnInfo = envid ? ` ENVID=${envid}` : '';
|
||||
socket.write(`250 OK: Message accepted with DSN${dsnInfo}\r\n`);
|
||||
|
||||
// Reset for next message
|
||||
envid = '';
|
||||
recipients.length = 0;
|
||||
} 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 DSN options
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'DSN test',
|
||||
text: 'Testing Delivery Status Notifications',
|
||||
dsn: {
|
||||
notify: ['SUCCESS', 'FAILURE', 'DELAY'],
|
||||
envid: 'unique-message-id-12345',
|
||||
ret: 'FULL'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(' DSN parameters accepted');
|
||||
expect(result).toBeDefined();
|
||||
expect(result.response).toContain('DSN');
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 6: ENHANCEDSTATUSCODES extension
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing ENHANCEDSTATUSCODES extension`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 enhanced.example.com ESMTP\r\n');
|
||||
|
||||
let useEnhancedCodes = false;
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-enhanced.example.com\r\n');
|
||||
socket.write('250-ENHANCEDSTATUSCODES\r\n');
|
||||
socket.write('250 2.0.0 OK\r\n');
|
||||
useEnhancedCodes = true;
|
||||
} else if (command.startsWith('HELO')) {
|
||||
socket.write('250 enhanced.example.com\r\n');
|
||||
useEnhancedCodes = false;
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
if (useEnhancedCodes) {
|
||||
socket.write('250 2.1.0 Sender OK\r\n');
|
||||
} else {
|
||||
socket.write('250 Sender OK\r\n');
|
||||
}
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
const address = command.match(/<(.+)>/)?.[1] || '';
|
||||
|
||||
if (address.includes('unknown')) {
|
||||
if (useEnhancedCodes) {
|
||||
socket.write('550 5.1.1 User unknown\r\n');
|
||||
} else {
|
||||
socket.write('550 User unknown\r\n');
|
||||
}
|
||||
} else {
|
||||
if (useEnhancedCodes) {
|
||||
socket.write('250 2.1.5 Recipient OK\r\n');
|
||||
} else {
|
||||
socket.write('250 Recipient OK\r\n');
|
||||
}
|
||||
}
|
||||
} else if (command === 'DATA') {
|
||||
if (useEnhancedCodes) {
|
||||
socket.write('354 2.0.0 Start mail input\r\n');
|
||||
} else {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
}
|
||||
} else if (command === '.') {
|
||||
if (useEnhancedCodes) {
|
||||
socket.write('250 2.0.0 Message accepted for delivery\r\n');
|
||||
} else {
|
||||
socket.write('250 Message accepted for delivery\r\n');
|
||||
}
|
||||
} else if (command === 'QUIT') {
|
||||
if (useEnhancedCodes) {
|
||||
socket.write('221 2.0.0 Service closing transmission channel\r\n');
|
||||
} else {
|
||||
socket.write('221 Service closing transmission channel\r\n');
|
||||
}
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Test with valid recipient
|
||||
const validEmail = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['valid@example.com'],
|
||||
subject: 'Enhanced status codes test',
|
||||
text: 'Testing enhanced SMTP status codes'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(validEmail);
|
||||
console.log(' Enhanced status codes received');
|
||||
expect(result).toBeDefined();
|
||||
expect(result.response).toMatch(/2\.\d\.\d/); // Enhanced code format
|
||||
|
||||
// Test with invalid recipient
|
||||
const invalidEmail = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['unknown@example.com'],
|
||||
subject: 'Invalid recipient test',
|
||||
text: 'Testing enhanced error codes'
|
||||
});
|
||||
|
||||
try {
|
||||
await smtpClient.sendMail(invalidEmail);
|
||||
console.log(' Unexpected: Invalid recipient accepted');
|
||||
} catch (error) {
|
||||
console.log(' Enhanced error code received');
|
||||
expect(error.responseCode).toBe(550);
|
||||
expect(error.response).toContain('5.1.1');
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
console.log(`\n${testId}: All ${scenarioCount} ESMTP compliance scenarios tested ✓`);
|
||||
});
|
@ -0,0 +1,411 @@
|
||||
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('CSEC-06: should validate TLS certificates correctly', async (tools) => {
|
||||
const testId = 'CSEC-06-certificate-validation';
|
||||
console.log(`\n${testId}: Testing TLS certificate validation...`);
|
||||
|
||||
let scenarioCount = 0;
|
||||
|
||||
// Scenario 1: Valid certificate acceptance
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing valid certificate acceptance`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
secure: true,
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Secure client connected');
|
||||
socket.write('220 secure.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-secure.example.com\r\n');
|
||||
socket.write('250-SIZE 10485760\r\n');
|
||||
socket.write('250 AUTH PLAIN LOGIN\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: Secure message accepted\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: true,
|
||||
tls: {
|
||||
rejectUnauthorized: false // Accept self-signed for test
|
||||
}
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Valid certificate test',
|
||||
text: 'Testing with valid TLS connection'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(` Result: ${result.messageId ? 'Success' : 'Failed'}`);
|
||||
console.log(' Certificate accepted for secure connection');
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 2: Self-signed certificate handling
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing self-signed certificate handling`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
secure: true,
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected with self-signed cert');
|
||||
socket.write('220 selfsigned.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-selfsigned.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Test with strict validation (should fail)
|
||||
const strictClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: true,
|
||||
tls: {
|
||||
rejectUnauthorized: true // Reject self-signed
|
||||
}
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Self-signed cert test',
|
||||
text: 'Testing self-signed certificate rejection'
|
||||
});
|
||||
|
||||
try {
|
||||
await strictClient.sendMail(email);
|
||||
console.log(' Unexpected: Self-signed cert was accepted');
|
||||
} catch (error) {
|
||||
console.log(` Expected error: ${error.message}`);
|
||||
expect(error.message).toContain('self signed');
|
||||
}
|
||||
|
||||
// Test with relaxed validation (should succeed)
|
||||
const relaxedClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: true,
|
||||
tls: {
|
||||
rejectUnauthorized: false // Accept self-signed
|
||||
}
|
||||
});
|
||||
|
||||
const result = await relaxedClient.sendMail(email);
|
||||
console.log(' Self-signed cert accepted with relaxed validation');
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 3: Certificate hostname verification
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing certificate hostname verification`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
secure: true,
|
||||
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();
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Connect with hostname verification
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: true,
|
||||
tls: {
|
||||
rejectUnauthorized: false, // For self-signed
|
||||
servername: testServer.hostname, // Verify hostname
|
||||
checkServerIdentity: (hostname, cert) => {
|
||||
console.log(` Verifying hostname: ${hostname}`);
|
||||
console.log(` Certificate CN: ${cert.subject?.CN || 'N/A'}`);
|
||||
// Custom verification logic could go here
|
||||
return undefined; // No error
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Hostname verification test',
|
||||
text: 'Testing certificate hostname matching'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(' Hostname verification completed');
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 4: Certificate expiration handling
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing certificate expiration handling`);
|
||||
|
||||
// Note: In a real test, we would use an expired certificate
|
||||
// For this test, we simulate the behavior
|
||||
|
||||
const testServer = await createTestServer({
|
||||
secure: true,
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 expired.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-expired.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: true,
|
||||
tls: {
|
||||
rejectUnauthorized: false,
|
||||
// Custom certificate validation
|
||||
secureContext: {
|
||||
cert: undefined,
|
||||
key: undefined,
|
||||
ca: undefined
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Certificate expiration test',
|
||||
text: 'Testing expired certificate handling'
|
||||
});
|
||||
|
||||
console.log(' Testing with potentially expired certificate...');
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(' Connection established (test environment)');
|
||||
expect(result).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 5: Certificate chain validation
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing certificate chain validation`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
secure: true,
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 chain.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-chain.example.com\r\n');
|
||||
socket.write('250-STARTTLS\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: true,
|
||||
tls: {
|
||||
rejectUnauthorized: false,
|
||||
// In production, would specify CA certificates
|
||||
ca: undefined,
|
||||
requestCert: true,
|
||||
// Log certificate details
|
||||
secureContext: undefined
|
||||
}
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Certificate chain test',
|
||||
text: 'Testing certificate chain validation'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(' Certificate chain validation completed');
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 6: Certificate pinning
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing certificate pinning`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
secure: true,
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 pinned.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-pinned.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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// In production, would pin specific certificate fingerprint
|
||||
const expectedFingerprint = 'SHA256:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: true,
|
||||
tls: {
|
||||
rejectUnauthorized: false,
|
||||
checkServerIdentity: (hostname, cert) => {
|
||||
// In production, would verify fingerprint
|
||||
console.log(` Certificate fingerprint: ${cert.fingerprint256 || 'N/A'}`);
|
||||
console.log(` Expected fingerprint: ${expectedFingerprint}`);
|
||||
|
||||
// For test, accept any certificate
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Certificate pinning test',
|
||||
text: 'Testing certificate fingerprint verification'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(' Certificate pinning check completed');
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
console.log(`\n${testId}: All ${scenarioCount} certificate validation scenarios tested ✓`);
|
||||
});
|
507
test/suite/smtpclient_security/test.csec-07.cipher-suites.ts
Normal file
507
test/suite/smtpclient_security/test.csec-07.cipher-suites.ts
Normal file
@ -0,0 +1,507 @@
|
||||
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('CSEC-07: should handle cipher suites correctly', async (tools) => {
|
||||
const testId = 'CSEC-07-cipher-suites';
|
||||
console.log(`\n${testId}: Testing cipher suite handling...`);
|
||||
|
||||
let scenarioCount = 0;
|
||||
|
||||
// Scenario 1: Strong cipher suite negotiation
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing strong cipher suite negotiation`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
secure: true,
|
||||
tlsOptions: {
|
||||
// Configure strong ciphers only
|
||||
ciphers: 'TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:ECDHE-RSA-AES256-GCM-SHA384',
|
||||
honorCipherOrder: true,
|
||||
minVersion: 'TLSv1.2'
|
||||
},
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected with strong ciphers');
|
||||
|
||||
// Log cipher info if available
|
||||
const tlsSocket = socket as any;
|
||||
if (tlsSocket.getCipher) {
|
||||
const cipher = tlsSocket.getCipher();
|
||||
console.log(` [Server] Negotiated cipher: ${cipher.name} (${cipher.version})`);
|
||||
}
|
||||
|
||||
socket.write('220 secure.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-secure.example.com\r\n');
|
||||
socket.write('250-SIZE 10485760\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 accepted with strong encryption\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: true,
|
||||
tls: {
|
||||
rejectUnauthorized: false,
|
||||
// Prefer strong ciphers
|
||||
ciphers: 'HIGH:!aNULL:!MD5:!3DES',
|
||||
minVersion: 'TLSv1.2'
|
||||
}
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Strong cipher test',
|
||||
text: 'Testing with strong cipher suites'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(' Successfully negotiated strong cipher');
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 2: Weak cipher rejection
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing weak cipher rejection`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
secure: true,
|
||||
tlsOptions: {
|
||||
// Only allow strong ciphers
|
||||
ciphers: 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256',
|
||||
honorCipherOrder: true,
|
||||
minVersion: 'TLSv1.2'
|
||||
},
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 secure.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-secure.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 connect with weak ciphers only (should fail)
|
||||
const weakClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: true,
|
||||
tls: {
|
||||
rejectUnauthorized: false,
|
||||
// Try to use weak ciphers
|
||||
ciphers: 'DES-CBC3-SHA:RC4-SHA',
|
||||
maxVersion: 'TLSv1.0'
|
||||
}
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Weak cipher test',
|
||||
text: 'Testing weak cipher rejection'
|
||||
});
|
||||
|
||||
try {
|
||||
await weakClient.sendMail(email);
|
||||
console.log(' Unexpected: Weak cipher was accepted');
|
||||
} catch (error) {
|
||||
console.log(` Expected error: ${error.message}`);
|
||||
expect(error.message).toMatch(/handshake|cipher|ssl/i);
|
||||
}
|
||||
|
||||
// Connect with acceptable ciphers
|
||||
const strongClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: true,
|
||||
tls: {
|
||||
rejectUnauthorized: false,
|
||||
ciphers: 'HIGH:!aNULL',
|
||||
minVersion: 'TLSv1.2'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await strongClient.sendMail(email);
|
||||
console.log(' Successfully connected with strong ciphers');
|
||||
expect(result).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 3: Cipher suite priority testing
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing cipher suite priority`);
|
||||
|
||||
const preferredCiphers = [
|
||||
'TLS_AES_256_GCM_SHA384',
|
||||
'TLS_AES_128_GCM_SHA256',
|
||||
'ECDHE-RSA-AES256-GCM-SHA384',
|
||||
'ECDHE-RSA-AES128-GCM-SHA256'
|
||||
];
|
||||
|
||||
const testServer = await createTestServer({
|
||||
secure: true,
|
||||
tlsOptions: {
|
||||
ciphers: preferredCiphers.join(':'),
|
||||
honorCipherOrder: true, // Server chooses cipher
|
||||
minVersion: 'TLSv1.2'
|
||||
},
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
|
||||
const tlsSocket = socket as any;
|
||||
if (tlsSocket.getCipher) {
|
||||
const cipher = tlsSocket.getCipher();
|
||||
console.log(` [Server] Selected cipher: ${cipher.name}`);
|
||||
|
||||
// Check if preferred cipher was selected
|
||||
const cipherIndex = preferredCiphers.findIndex(c =>
|
||||
cipher.name.includes(c) || c.includes(cipher.name)
|
||||
);
|
||||
console.log(` [Server] Cipher priority: ${cipherIndex + 1}/${preferredCiphers.length}`);
|
||||
}
|
||||
|
||||
socket.write('220 priority.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-priority.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: true,
|
||||
tls: {
|
||||
rejectUnauthorized: false,
|
||||
// Client offers ciphers in different order
|
||||
ciphers: preferredCiphers.slice().reverse().join(':'),
|
||||
honorCipherOrder: false // Let server choose
|
||||
}
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Cipher priority test',
|
||||
text: 'Testing cipher suite selection priority'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(' Cipher negotiation completed');
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 4: Perfect Forward Secrecy (PFS) ciphers
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing Perfect Forward Secrecy ciphers`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
secure: true,
|
||||
tlsOptions: {
|
||||
// Only PFS ciphers (ECDHE/DHE)
|
||||
ciphers: 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384',
|
||||
honorCipherOrder: true,
|
||||
ecdhCurve: 'auto'
|
||||
},
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected with PFS');
|
||||
|
||||
const tlsSocket = socket as any;
|
||||
if (tlsSocket.getCipher) {
|
||||
const cipher = tlsSocket.getCipher();
|
||||
const hasPFS = cipher.name.includes('ECDHE') || cipher.name.includes('DHE');
|
||||
console.log(` [Server] Cipher: ${cipher.name} (PFS: ${hasPFS ? 'Yes' : 'No'})`);
|
||||
}
|
||||
|
||||
socket.write('220 pfs.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-pfs.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 sent with PFS\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: true,
|
||||
tls: {
|
||||
rejectUnauthorized: false,
|
||||
// Prefer PFS ciphers
|
||||
ciphers: 'ECDHE:DHE:!aNULL:!MD5',
|
||||
ecdhCurve: 'auto'
|
||||
}
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'PFS cipher test',
|
||||
text: 'Testing Perfect Forward Secrecy'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(' Successfully used PFS cipher');
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 5: Cipher renegotiation
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing cipher renegotiation handling`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
secure: true,
|
||||
tlsOptions: {
|
||||
ciphers: 'HIGH:MEDIUM:!aNULL:!MD5',
|
||||
// Disable renegotiation for security
|
||||
secureOptions: plugins.crypto.constants.SSL_OP_NO_RENEGOTIATION
|
||||
},
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 norenegotiation.example.com ESMTP\r\n');
|
||||
|
||||
let messageCount = 0;
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-norenegotiation.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
messageCount++;
|
||||
console.log(` [Server] Processing message ${messageCount}`);
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'RSET') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: true,
|
||||
tls: {
|
||||
rejectUnauthorized: false,
|
||||
// Also disable renegotiation on client
|
||||
secureOptions: plugins.crypto.constants.SSL_OP_NO_RENEGOTIATION
|
||||
}
|
||||
});
|
||||
|
||||
// Send multiple emails on same connection
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: `Renegotiation test ${i + 1}`,
|
||||
text: `Testing without cipher renegotiation - email ${i + 1}`
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(` Email ${i + 1} sent without renegotiation`);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 6: Cipher compatibility testing
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing cipher compatibility`);
|
||||
|
||||
const cipherSets = [
|
||||
{
|
||||
name: 'TLS 1.3 only',
|
||||
ciphers: 'TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256',
|
||||
minVersion: 'TLSv1.3',
|
||||
maxVersion: 'TLSv1.3'
|
||||
},
|
||||
{
|
||||
name: 'TLS 1.2 compatible',
|
||||
ciphers: 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256',
|
||||
minVersion: 'TLSv1.2',
|
||||
maxVersion: 'TLSv1.2'
|
||||
},
|
||||
{
|
||||
name: 'Broad compatibility',
|
||||
ciphers: 'HIGH:MEDIUM:!aNULL:!MD5:!3DES',
|
||||
minVersion: 'TLSv1.2',
|
||||
maxVersion: undefined
|
||||
}
|
||||
];
|
||||
|
||||
for (const cipherSet of cipherSets) {
|
||||
console.log(`\n Testing ${cipherSet.name}...`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
secure: true,
|
||||
tlsOptions: {
|
||||
ciphers: cipherSet.ciphers,
|
||||
minVersion: cipherSet.minVersion as any,
|
||||
maxVersion: cipherSet.maxVersion as any
|
||||
},
|
||||
onConnection: async (socket) => {
|
||||
const tlsSocket = socket as any;
|
||||
if (tlsSocket.getCipher && tlsSocket.getProtocol) {
|
||||
const cipher = tlsSocket.getCipher();
|
||||
const protocol = tlsSocket.getProtocol();
|
||||
console.log(` [Server] Protocol: ${protocol}, Cipher: ${cipher.name}`);
|
||||
}
|
||||
|
||||
socket.write('220 compat.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-compat.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 {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: true,
|
||||
tls: {
|
||||
rejectUnauthorized: false,
|
||||
ciphers: cipherSet.ciphers,
|
||||
minVersion: cipherSet.minVersion as any,
|
||||
maxVersion: cipherSet.maxVersion as any
|
||||
}
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: `${cipherSet.name} test`,
|
||||
text: `Testing ${cipherSet.name} cipher configuration`
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(` Success with ${cipherSet.name}`);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
} catch (error) {
|
||||
console.log(` ${cipherSet.name} not supported in this environment`);
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
}
|
||||
})();
|
||||
|
||||
console.log(`\n${testId}: All ${scenarioCount} cipher suite scenarios tested ✓`);
|
||||
});
|
@ -0,0 +1,562 @@
|
||||
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('CSEC-08: should handle authentication fallback securely', async (tools) => {
|
||||
const testId = 'CSEC-08-authentication-fallback';
|
||||
console.log(`\n${testId}: Testing authentication fallback mechanisms...`);
|
||||
|
||||
let scenarioCount = 0;
|
||||
|
||||
// Scenario 1: Multiple authentication methods
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing multiple authentication methods`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 auth.example.com ESMTP\r\n');
|
||||
|
||||
let authMethod = '';
|
||||
let authStep = 0;
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-auth.example.com\r\n');
|
||||
socket.write('250-AUTH CRAM-MD5 PLAIN LOGIN\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('AUTH CRAM-MD5')) {
|
||||
authMethod = 'CRAM-MD5';
|
||||
authStep = 1;
|
||||
// Send challenge
|
||||
const challenge = Buffer.from(`<${Date.now()}.${Math.random()}@auth.example.com>`).toString('base64');
|
||||
socket.write(`334 ${challenge}\r\n`);
|
||||
} else if (command.startsWith('AUTH PLAIN')) {
|
||||
authMethod = 'PLAIN';
|
||||
if (command.length > 11) {
|
||||
// Credentials included
|
||||
const credentials = Buffer.from(command.substring(11), 'base64').toString();
|
||||
console.log(` [Server] PLAIN auth attempt with immediate credentials`);
|
||||
socket.write('235 2.7.0 Authentication successful\r\n');
|
||||
} else {
|
||||
// Request credentials
|
||||
socket.write('334\r\n');
|
||||
}
|
||||
} else if (command.startsWith('AUTH LOGIN')) {
|
||||
authMethod = 'LOGIN';
|
||||
authStep = 1;
|
||||
socket.write('334 VXNlcm5hbWU6\r\n'); // Username:
|
||||
} else if (authMethod === 'CRAM-MD5' && authStep === 1) {
|
||||
// Verify CRAM-MD5 response
|
||||
console.log(` [Server] CRAM-MD5 response received`);
|
||||
socket.write('235 2.7.0 Authentication successful\r\n');
|
||||
authMethod = '';
|
||||
authStep = 0;
|
||||
} else if (authMethod === 'PLAIN' && !command.startsWith('AUTH')) {
|
||||
// PLAIN credentials
|
||||
const credentials = Buffer.from(command, 'base64').toString();
|
||||
console.log(` [Server] PLAIN credentials received`);
|
||||
socket.write('235 2.7.0 Authentication successful\r\n');
|
||||
authMethod = '';
|
||||
} else if (authMethod === 'LOGIN' && authStep === 1) {
|
||||
// Username
|
||||
console.log(` [Server] LOGIN username received`);
|
||||
authStep = 2;
|
||||
socket.write('334 UGFzc3dvcmQ6\r\n'); // Password:
|
||||
} else if (authMethod === 'LOGIN' && authStep === 2) {
|
||||
// Password
|
||||
console.log(` [Server] LOGIN password received`);
|
||||
socket.write('235 2.7.0 Authentication successful\r\n');
|
||||
authMethod = '';
|
||||
authStep = 0;
|
||||
} 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,
|
||||
auth: {
|
||||
user: 'testuser',
|
||||
pass: 'testpass'
|
||||
}
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Multi-auth test',
|
||||
text: 'Testing multiple authentication methods'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(' Authentication successful');
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 2: Authentication method downgrade prevention
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing auth method downgrade prevention`);
|
||||
|
||||
let attemptCount = 0;
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 secure.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
attemptCount++;
|
||||
if (attemptCount === 1) {
|
||||
// First attempt: offer secure methods
|
||||
socket.write('250-secure.example.com\r\n');
|
||||
socket.write('250-AUTH CRAM-MD5 SCRAM-SHA-256\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else {
|
||||
// Attacker attempt: offer weaker methods
|
||||
socket.write('250-secure.example.com\r\n');
|
||||
socket.write('250-AUTH PLAIN LOGIN\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else if (command.startsWith('AUTH CRAM-MD5')) {
|
||||
// Simulate failure to force fallback attempt
|
||||
socket.write('535 5.7.8 Authentication failed\r\n');
|
||||
} else if (command.startsWith('AUTH PLAIN') || command.startsWith('AUTH LOGIN')) {
|
||||
console.log(' [Server] Warning: Client using weak auth method');
|
||||
socket.write('535 5.7.8 Weak authentication method not allowed\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,
|
||||
auth: {
|
||||
user: 'testuser',
|
||||
pass: 'testpass'
|
||||
},
|
||||
authMethod: 'CRAM-MD5' // Prefer secure method
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Downgrade prevention test',
|
||||
text: 'Testing authentication downgrade prevention'
|
||||
});
|
||||
|
||||
try {
|
||||
await smtpClient.sendMail(email);
|
||||
console.log(' Unexpected: Authentication succeeded');
|
||||
} catch (error) {
|
||||
console.log(` Expected: Auth failed - ${error.message}`);
|
||||
expect(error.message).toContain('Authentication failed');
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 3: OAuth2 fallback
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing OAuth2 authentication fallback`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 oauth.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command.substring(0, 50)}...`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-oauth.example.com\r\n');
|
||||
socket.write('250-AUTH XOAUTH2 PLAIN LOGIN\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('AUTH XOAUTH2')) {
|
||||
// Check OAuth2 token
|
||||
const token = command.substring(13);
|
||||
if (token.includes('expired')) {
|
||||
console.log(' [Server] OAuth2 token expired');
|
||||
socket.write('334 eyJzdGF0dXMiOiI0MDEiLCJzY2hlbWVzIjoiYmVhcmVyIiwic2NvcGUiOiJodHRwczovL21haWwuZ29vZ2xlLmNvbS8ifQ==\r\n');
|
||||
} else {
|
||||
console.log(' [Server] OAuth2 authentication successful');
|
||||
socket.write('235 2.7.0 Authentication successful\r\n');
|
||||
}
|
||||
} else if (command.startsWith('AUTH PLAIN')) {
|
||||
// Fallback to PLAIN auth
|
||||
console.log(' [Server] Fallback to PLAIN auth');
|
||||
if (command.length > 11) {
|
||||
socket.write('235 2.7.0 Authentication successful\r\n');
|
||||
} else {
|
||||
socket.write('334\r\n');
|
||||
}
|
||||
} else if (command === '') {
|
||||
// Empty line after failed XOAUTH2
|
||||
socket.write('535 5.7.8 Authentication failed\r\n');
|
||||
} else if (!command.startsWith('AUTH') && command.length > 20) {
|
||||
// PLAIN credentials
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Test with OAuth2 token
|
||||
const oauthClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
auth: {
|
||||
type: 'oauth2',
|
||||
user: 'user@example.com',
|
||||
accessToken: 'valid-oauth-token'
|
||||
}
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'OAuth2 test',
|
||||
text: 'Testing OAuth2 authentication'
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await oauthClient.sendMail(email);
|
||||
console.log(' OAuth2 authentication successful');
|
||||
expect(result).toBeDefined();
|
||||
} catch (error) {
|
||||
console.log(` OAuth2 failed, testing fallback...`);
|
||||
|
||||
// Test fallback to password auth
|
||||
const fallbackClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
auth: {
|
||||
user: 'testuser',
|
||||
pass: 'testpass'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await fallbackClient.sendMail(email);
|
||||
console.log(' Fallback authentication successful');
|
||||
expect(result).toBeDefined();
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 4: Authentication retry with different credentials
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing auth retry with different credentials`);
|
||||
|
||||
let authAttempts = 0;
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 retry.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-retry.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}`);
|
||||
|
||||
if (authAttempts <= 2) {
|
||||
// Fail first attempts
|
||||
socket.write('535 5.7.8 Authentication failed\r\n');
|
||||
} else {
|
||||
// Success on third attempt
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Test multiple auth attempts
|
||||
const credentials = [
|
||||
{ user: 'wronguser', pass: 'wrongpass' },
|
||||
{ user: 'testuser', pass: 'wrongpass' },
|
||||
{ user: 'testuser', pass: 'testpass' }
|
||||
];
|
||||
|
||||
let successfulAuth = false;
|
||||
|
||||
for (const cred of credentials) {
|
||||
try {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
auth: cred
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Retry test',
|
||||
text: 'Testing authentication retry'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(` Auth succeeded with user: ${cred.user}`);
|
||||
successfulAuth = true;
|
||||
expect(result).toBeDefined();
|
||||
break;
|
||||
} catch (error) {
|
||||
console.log(` Auth failed with user: ${cred.user}`);
|
||||
}
|
||||
}
|
||||
|
||||
expect(successfulAuth).toBe(true);
|
||||
expect(authAttempts).toBe(3);
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 5: Secure authentication over insecure connection
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing secure auth over insecure connection`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
secure: false, // Plain text connection
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected (insecure)');
|
||||
socket.write('220 insecure.example.com ESMTP\r\n');
|
||||
|
||||
let tlsStarted = false;
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-insecure.example.com\r\n');
|
||||
socket.write('250-STARTTLS\r\n');
|
||||
if (tlsStarted) {
|
||||
socket.write('250-AUTH PLAIN LOGIN\r\n');
|
||||
}
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'STARTTLS') {
|
||||
socket.write('220 2.0.0 Ready to start TLS\r\n');
|
||||
tlsStarted = true;
|
||||
// In real scenario, would upgrade to TLS here
|
||||
} else if (command.startsWith('AUTH') && !tlsStarted) {
|
||||
console.log(' [Server] Rejecting auth over insecure connection');
|
||||
socket.write('530 5.7.0 Must issue a STARTTLS command first\r\n');
|
||||
} else if (command.startsWith('AUTH') && tlsStarted) {
|
||||
console.log(' [Server] Accepting auth over TLS');
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Try auth without TLS (should fail)
|
||||
const insecureClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
ignoreTLS: true, // Don't use STARTTLS
|
||||
auth: {
|
||||
user: 'testuser',
|
||||
pass: 'testpass'
|
||||
}
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Secure auth test',
|
||||
text: 'Testing secure authentication requirements'
|
||||
});
|
||||
|
||||
try {
|
||||
await insecureClient.sendMail(email);
|
||||
console.log(' Unexpected: Auth succeeded without TLS');
|
||||
} catch (error) {
|
||||
console.log(' Expected: Auth rejected without TLS');
|
||||
expect(error.message).toContain('STARTTLS');
|
||||
}
|
||||
|
||||
// Try with STARTTLS
|
||||
const secureClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
requireTLS: true,
|
||||
auth: {
|
||||
user: 'testuser',
|
||||
pass: 'testpass'
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await secureClient.sendMail(email);
|
||||
console.log(' Auth succeeded with STARTTLS');
|
||||
// Note: In real test, STARTTLS would actually upgrade the connection
|
||||
} catch (error) {
|
||||
console.log(' STARTTLS not fully implemented in test');
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 6: Authentication mechanism negotiation
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing auth mechanism negotiation`);
|
||||
|
||||
const supportedMechanisms = new Map([
|
||||
['SCRAM-SHA-256', { priority: 1, supported: true }],
|
||||
['CRAM-MD5', { priority: 2, supported: true }],
|
||||
['PLAIN', { priority: 3, supported: true }],
|
||||
['LOGIN', { priority: 4, supported: true }]
|
||||
]);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 negotiate.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-negotiate.example.com\r\n');
|
||||
const authMechs = Array.from(supportedMechanisms.entries())
|
||||
.filter(([_, info]) => info.supported)
|
||||
.map(([mech, _]) => mech)
|
||||
.join(' ');
|
||||
socket.write(`250-AUTH ${authMechs}\r\n`);
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('AUTH ')) {
|
||||
const mechanism = command.split(' ')[1];
|
||||
console.log(` [Server] Client selected: ${mechanism}`);
|
||||
|
||||
const mechInfo = supportedMechanisms.get(mechanism);
|
||||
if (mechInfo && mechInfo.supported) {
|
||||
console.log(` [Server] Priority: ${mechInfo.priority}`);
|
||||
socket.write('235 2.7.0 Authentication successful\r\n');
|
||||
} else {
|
||||
socket.write('504 5.5.4 Unrecognized authentication type\r\n');
|
||||
}
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
auth: {
|
||||
user: 'testuser',
|
||||
pass: 'testpass'
|
||||
}
|
||||
// Client will negotiate best available mechanism
|
||||
});
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Negotiation test',
|
||||
text: 'Testing authentication mechanism negotiation'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(' Authentication negotiation successful');
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
console.log(`\n${testId}: All ${scenarioCount} authentication fallback scenarios tested ✓`);
|
||||
});
|
@ -0,0 +1,627 @@
|
||||
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('CSEC-09: should handle relay restrictions correctly', async (tools) => {
|
||||
const testId = 'CSEC-09-relay-restrictions';
|
||||
console.log(`\n${testId}: Testing relay restriction handling...`);
|
||||
|
||||
let scenarioCount = 0;
|
||||
|
||||
// Scenario 1: Open relay prevention
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing open relay prevention`);
|
||||
|
||||
const allowedDomains = ['example.com', 'trusted.com'];
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 relay.example.com ESMTP\r\n');
|
||||
|
||||
let authenticated = false;
|
||||
let fromAddress = '';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-relay.example.com\r\n');
|
||||
socket.write('250-AUTH PLAIN LOGIN\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('AUTH')) {
|
||||
authenticated = true;
|
||||
console.log(' [Server] User authenticated');
|
||||
socket.write('235 2.7.0 Authentication successful\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
fromAddress = command.match(/<(.+)>/)?.[1] || '';
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
const toAddress = command.match(/<(.+)>/)?.[1] || '';
|
||||
const toDomain = toAddress.split('@')[1];
|
||||
const fromDomain = fromAddress.split('@')[1];
|
||||
|
||||
console.log(` [Server] Relay check: from=${fromDomain}, to=${toDomain}, auth=${authenticated}`);
|
||||
|
||||
// Check relay permissions
|
||||
if (authenticated) {
|
||||
// Authenticated users can relay
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (allowedDomains.includes(toDomain)) {
|
||||
// Accept mail for local domains
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (allowedDomains.includes(fromDomain)) {
|
||||
// Accept mail from local domains (outbound)
|
||||
socket.write('250 OK\r\n');
|
||||
} else {
|
||||
// Reject relay attempt
|
||||
console.log(' [Server] Rejecting relay attempt');
|
||||
socket.write('554 5.7.1 Relay access denied\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 1: Unauthenticated relay attempt (should fail)
|
||||
const unauthClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
const relayEmail = new plugins.smartmail.Email({
|
||||
from: 'external@untrusted.com',
|
||||
to: ['recipient@another-external.com'],
|
||||
subject: 'Relay test',
|
||||
text: 'Testing open relay prevention'
|
||||
});
|
||||
|
||||
try {
|
||||
await unauthClient.sendMail(relayEmail);
|
||||
console.log(' Unexpected: Relay was allowed');
|
||||
} catch (error) {
|
||||
console.log(' Expected: Relay denied for unauthenticated user');
|
||||
expect(error.message).toContain('Relay access denied');
|
||||
}
|
||||
|
||||
// Test 2: Local delivery (should succeed)
|
||||
const localEmail = new plugins.smartmail.Email({
|
||||
from: 'external@untrusted.com',
|
||||
to: ['recipient@example.com'], // Local domain
|
||||
subject: 'Local delivery test',
|
||||
text: 'Testing local delivery'
|
||||
});
|
||||
|
||||
const localResult = await unauthClient.sendMail(localEmail);
|
||||
console.log(' Local delivery allowed');
|
||||
expect(localResult).toBeDefined();
|
||||
expect(localResult.messageId).toBeDefined();
|
||||
|
||||
// Test 3: Authenticated relay (should succeed)
|
||||
const authClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
auth: {
|
||||
user: 'testuser',
|
||||
pass: 'testpass'
|
||||
}
|
||||
});
|
||||
|
||||
const authRelayResult = await authClient.sendMail(relayEmail);
|
||||
console.log(' Authenticated relay allowed');
|
||||
expect(authRelayResult).toBeDefined();
|
||||
expect(authRelayResult.messageId).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 2: IP-based relay restrictions
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing IP-based relay restrictions`);
|
||||
|
||||
const trustedIPs = ['127.0.0.1', '::1', '10.0.0.0/8', '192.168.0.0/16'];
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
const clientIP = socket.remoteAddress || '';
|
||||
console.log(` [Server] Client connected from ${clientIP}`);
|
||||
socket.write('220 ip-relay.example.com ESMTP\r\n');
|
||||
|
||||
const isTrustedIP = (ip: string): boolean => {
|
||||
// Simple check for demo (in production, use proper IP range checking)
|
||||
return trustedIPs.some(trusted =>
|
||||
ip === trusted ||
|
||||
ip.includes('127.0.0.1') ||
|
||||
ip.includes('::1')
|
||||
);
|
||||
};
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-ip-relay.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
const toAddress = command.match(/<(.+)>/)?.[1] || '';
|
||||
const isLocalDomain = toAddress.includes('@example.com');
|
||||
|
||||
if (isTrustedIP(clientIP)) {
|
||||
console.log(' [Server] Trusted IP - allowing relay');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (isLocalDomain) {
|
||||
console.log(' [Server] Local delivery - allowing');
|
||||
socket.write('250 OK\r\n');
|
||||
} else {
|
||||
console.log(' [Server] Untrusted IP - denying relay');
|
||||
socket.write('554 5.7.1 Relay access denied for IP\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 from localhost (trusted)
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@external.com'],
|
||||
subject: 'IP-based relay test',
|
||||
text: 'Testing IP-based relay restrictions'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(' Relay allowed from trusted IP (localhost)');
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messageId).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 3: Sender domain restrictions
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing sender domain restrictions`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 sender-restrict.example.com ESMTP\r\n');
|
||||
|
||||
let authenticated = false;
|
||||
let authUser = '';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-sender-restrict.example.com\r\n');
|
||||
socket.write('250-AUTH PLAIN\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('AUTH PLAIN')) {
|
||||
const credentials = command.substring(11);
|
||||
if (credentials) {
|
||||
const decoded = Buffer.from(credentials, 'base64').toString();
|
||||
authUser = decoded.split('\0')[1] || '';
|
||||
authenticated = true;
|
||||
console.log(` [Server] User authenticated: ${authUser}`);
|
||||
socket.write('235 2.7.0 Authentication successful\r\n');
|
||||
} else {
|
||||
socket.write('334\r\n');
|
||||
}
|
||||
} else if (!command.startsWith('AUTH') && authenticated === false && command.length > 20) {
|
||||
// PLAIN auth credentials
|
||||
const decoded = Buffer.from(command, 'base64').toString();
|
||||
authUser = decoded.split('\0')[1] || '';
|
||||
authenticated = true;
|
||||
console.log(` [Server] User authenticated: ${authUser}`);
|
||||
socket.write('235 2.7.0 Authentication successful\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
const fromAddress = command.match(/<(.+)>/)?.[1] || '';
|
||||
const fromDomain = fromAddress.split('@')[1];
|
||||
|
||||
if (!authenticated) {
|
||||
// Unauthenticated users can only send from specific domains
|
||||
if (fromDomain === 'example.com' || fromDomain === 'trusted.com') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else {
|
||||
console.log(` [Server] Rejecting sender domain: ${fromDomain}`);
|
||||
socket.write('553 5.7.1 Sender domain not allowed\r\n');
|
||||
}
|
||||
} else {
|
||||
// Authenticated users must use their own domain
|
||||
const expectedDomain = authUser.split('@')[1];
|
||||
if (fromDomain === expectedDomain || fromDomain === 'example.com') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else {
|
||||
console.log(` [Server] Auth user ${authUser} cannot send from ${fromDomain}`);
|
||||
socket.write('553 5.7.1 Authenticated sender mismatch\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 1: Unauthorized sender domain
|
||||
const unauthClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
const unauthorizedEmail = new plugins.smartmail.Email({
|
||||
from: 'sender@untrusted.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Unauthorized sender test',
|
||||
text: 'Testing sender domain restrictions'
|
||||
});
|
||||
|
||||
try {
|
||||
await unauthClient.sendMail(unauthorizedEmail);
|
||||
console.log(' Unexpected: Unauthorized sender accepted');
|
||||
} catch (error) {
|
||||
console.log(' Expected: Unauthorized sender domain rejected');
|
||||
expect(error.message).toContain('Sender domain not allowed');
|
||||
}
|
||||
|
||||
// Test 2: Authorized sender domain
|
||||
const authorizedEmail = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@external.com'],
|
||||
subject: 'Authorized sender test',
|
||||
text: 'Testing authorized sender domain'
|
||||
});
|
||||
|
||||
const result = await unauthClient.sendMail(authorizedEmail);
|
||||
console.log(' Authorized sender domain accepted');
|
||||
expect(result).toBeDefined();
|
||||
|
||||
// Test 3: Authenticated sender mismatch
|
||||
const authClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
auth: {
|
||||
user: 'user@example.com',
|
||||
pass: 'testpass'
|
||||
}
|
||||
});
|
||||
|
||||
const mismatchEmail = new plugins.smartmail.Email({
|
||||
from: 'someone@otherdomain.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Sender mismatch test',
|
||||
text: 'Testing authenticated sender mismatch'
|
||||
});
|
||||
|
||||
try {
|
||||
await authClient.sendMail(mismatchEmail);
|
||||
console.log(' Unexpected: Sender mismatch accepted');
|
||||
} catch (error) {
|
||||
console.log(' Expected: Authenticated sender mismatch rejected');
|
||||
expect(error.message).toContain('Authenticated sender mismatch');
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 4: Recipient count limits
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing recipient count limits`);
|
||||
|
||||
const maxRecipientsUnauthenticated = 5;
|
||||
const maxRecipientsAuthenticated = 100;
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 recipient-limit.example.com ESMTP\r\n');
|
||||
|
||||
let authenticated = false;
|
||||
let recipientCount = 0;
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-recipient-limit.example.com\r\n');
|
||||
socket.write('250-AUTH PLAIN\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('AUTH')) {
|
||||
authenticated = true;
|
||||
socket.write('235 2.7.0 Authentication successful\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
recipientCount = 0; // Reset for new message
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
recipientCount++;
|
||||
const limit = authenticated ? maxRecipientsAuthenticated : maxRecipientsUnauthenticated;
|
||||
|
||||
console.log(` [Server] Recipient ${recipientCount}/${limit} (auth: ${authenticated})`);
|
||||
|
||||
if (recipientCount > limit) {
|
||||
socket.write('452 4.5.3 Too many recipients\r\n');
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Test unauthenticated recipient limit
|
||||
const unauthClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
const manyRecipients = Array(10).fill(null).map((_, i) => `recipient${i + 1}@example.com`);
|
||||
|
||||
const bulkEmail = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: manyRecipients,
|
||||
subject: 'Recipient limit test',
|
||||
text: 'Testing recipient count limits'
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await unauthClient.sendMail(bulkEmail);
|
||||
console.log(` Sent to ${result.accepted?.length || 0} recipients (unauthenticated)`);
|
||||
// Some recipients should be rejected
|
||||
expect(result.rejected?.length).toBeGreaterThan(0);
|
||||
} catch (error) {
|
||||
console.log(' Some recipients rejected due to limit');
|
||||
}
|
||||
|
||||
// Test authenticated higher limit
|
||||
const authClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
auth: {
|
||||
user: 'testuser',
|
||||
pass: 'testpass'
|
||||
}
|
||||
});
|
||||
|
||||
const authResult = await authClient.sendMail(bulkEmail);
|
||||
console.log(` Authenticated user sent to ${authResult.accepted?.length || 0} recipients`);
|
||||
expect(authResult.accepted?.length).toBe(manyRecipients.length);
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 5: Rate-based relay restrictions
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing rate-based relay restrictions`);
|
||||
|
||||
const messageRates = new Map<string, { count: number; resetTime: number }>();
|
||||
const rateLimit = 3; // 3 messages per minute
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
const clientIP = socket.remoteAddress || 'unknown';
|
||||
console.log(` [Server] Client connected from ${clientIP}`);
|
||||
socket.write('220 rate-limit.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-rate-limit.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
const now = Date.now();
|
||||
const clientRate = messageRates.get(clientIP) || { count: 0, resetTime: now + 60000 };
|
||||
|
||||
if (now > clientRate.resetTime) {
|
||||
// Reset rate limit
|
||||
clientRate.count = 0;
|
||||
clientRate.resetTime = now + 60000;
|
||||
}
|
||||
|
||||
clientRate.count++;
|
||||
messageRates.set(clientIP, clientRate);
|
||||
|
||||
console.log(` [Server] Message ${clientRate.count}/${rateLimit} from ${clientIP}`);
|
||||
|
||||
if (clientRate.count > rateLimit) {
|
||||
socket.write('421 4.7.0 Rate limit exceeded, try again later\r\n');
|
||||
socket.end();
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Send multiple messages to test rate limiting
|
||||
const results: boolean[] = [];
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
try {
|
||||
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: `Rate test ${i + 1}`,
|
||||
text: `Testing rate limits - message ${i + 1}`
|
||||
});
|
||||
|
||||
const result = await client.sendMail(email);
|
||||
console.log(` Message ${i + 1}: Sent successfully`);
|
||||
results.push(true);
|
||||
} catch (error) {
|
||||
console.log(` Message ${i + 1}: Rate limited`);
|
||||
results.push(false);
|
||||
}
|
||||
}
|
||||
|
||||
// First 3 should succeed, rest should fail
|
||||
const successCount = results.filter(r => r).length;
|
||||
console.log(` Sent ${successCount}/${results.length} messages before rate limit`);
|
||||
expect(successCount).toBe(rateLimit);
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 6: SPF-based relay restrictions
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing SPF-based relay restrictions`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
const clientIP = socket.remoteAddress || '';
|
||||
console.log(` [Server] Client connected from ${clientIP}`);
|
||||
socket.write('220 spf-relay.example.com ESMTP\r\n');
|
||||
|
||||
const checkSPF = (domain: string, ip: string): string => {
|
||||
// Simplified SPF check for demo
|
||||
console.log(` [Server] Checking SPF for ${domain} from ${ip}`);
|
||||
|
||||
// In production, would do actual DNS lookups
|
||||
if (domain === 'example.com' && (ip.includes('127.0.0.1') || ip.includes('::1'))) {
|
||||
return 'pass';
|
||||
} else if (domain === 'spf-fail.com') {
|
||||
return 'fail';
|
||||
} else {
|
||||
return 'none';
|
||||
}
|
||||
};
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-spf-relay.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
const fromAddress = command.match(/<(.+)>/)?.[1] || '';
|
||||
const domain = fromAddress.split('@')[1];
|
||||
|
||||
const spfResult = checkSPF(domain, clientIP);
|
||||
console.log(` [Server] SPF result: ${spfResult}`);
|
||||
|
||||
if (spfResult === 'fail') {
|
||||
socket.write('550 5.7.1 SPF check failed\r\n');
|
||||
} else {
|
||||
socket.write('250 OK SPF=' + spfResult + '\r\n');
|
||||
}
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Test 1: SPF pass
|
||||
const spfPassEmail = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'SPF pass test',
|
||||
text: 'Testing SPF-based relay - should pass'
|
||||
});
|
||||
|
||||
const passResult = await smtpClient.sendMail(spfPassEmail);
|
||||
console.log(' SPF check passed');
|
||||
expect(passResult).toBeDefined();
|
||||
expect(passResult.response).toContain('SPF=pass');
|
||||
|
||||
// Test 2: SPF fail
|
||||
const spfFailEmail = new plugins.smartmail.Email({
|
||||
from: 'sender@spf-fail.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'SPF fail test',
|
||||
text: 'Testing SPF-based relay - should fail'
|
||||
});
|
||||
|
||||
try {
|
||||
await smtpClient.sendMail(spfFailEmail);
|
||||
console.log(' Unexpected: SPF fail was accepted');
|
||||
} catch (error) {
|
||||
console.log(' Expected: SPF check failed');
|
||||
expect(error.message).toContain('SPF check failed');
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
console.log(`\n${testId}: All ${scenarioCount} relay restriction scenarios tested ✓`);
|
||||
});
|
@ -0,0 +1,701 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as plugins from './plugins.js';
|
||||
import { createTestServer } from '../../helpers/server.loader.js';
|
||||
import { createSmtpClient } from '../../helpers/smtp.client.js';
|
||||
|
||||
tap.test('CSEC-10: should handle anti-spam measures correctly', async (tools) => {
|
||||
const testId = 'CSEC-10-anti-spam-measures';
|
||||
console.log(`\n${testId}: Testing anti-spam measure handling...`);
|
||||
|
||||
let scenarioCount = 0;
|
||||
|
||||
// Scenario 1: Reputation-based filtering
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing reputation-based filtering`);
|
||||
|
||||
const ipReputation = new Map([
|
||||
['127.0.0.1', { score: 100, status: 'trusted' }],
|
||||
['10.0.0.1', { score: 50, status: 'neutral' }],
|
||||
['192.168.1.100', { score: 10, status: 'suspicious' }],
|
||||
['10.10.10.10', { score: 0, status: 'blocked' }]
|
||||
]);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
const clientIP = socket.remoteAddress || '127.0.0.1';
|
||||
const reputation = ipReputation.get(clientIP) || { score: 50, status: 'unknown' };
|
||||
|
||||
console.log(` [Server] Client ${clientIP} connected (reputation: ${reputation.status})`);
|
||||
|
||||
if (reputation.score === 0) {
|
||||
socket.write('554 5.7.1 Your IP has been blocked due to poor reputation\r\n');
|
||||
socket.end();
|
||||
return;
|
||||
}
|
||||
|
||||
socket.write('220 reputation.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-reputation.example.com\r\n');
|
||||
if (reputation.score < 30) {
|
||||
// Suspicious IPs get limited features
|
||||
socket.write('250 OK\r\n');
|
||||
} else {
|
||||
socket.write('250-SIZE 10485760\r\n');
|
||||
socket.write('250-AUTH PLAIN LOGIN\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
if (reputation.score < 30) {
|
||||
// Add delay for suspicious IPs (tarpitting)
|
||||
setTimeout(() => {
|
||||
socket.write('250 OK\r\n');
|
||||
}, 2000);
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write(`250 OK: Message accepted (reputation score: ${reputation.score})\r\n`);
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Test with good reputation (localhost)
|
||||
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: 'Reputation test',
|
||||
text: 'Testing reputation-based filtering'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(' Good reputation: Message accepted');
|
||||
expect(result).toBeDefined();
|
||||
expect(result.response).toContain('reputation score: 100');
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 2: Content filtering and spam scoring
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing content filtering and spam scoring`);
|
||||
|
||||
const spamKeywords = [
|
||||
{ word: 'viagra', score: 5 },
|
||||
{ word: 'lottery', score: 4 },
|
||||
{ word: 'winner', score: 3 },
|
||||
{ word: 'click here', score: 3 },
|
||||
{ word: 'free money', score: 5 },
|
||||
{ word: 'guarantee', score: 2 },
|
||||
{ word: 'act now', score: 3 },
|
||||
{ word: '100% free', score: 4 }
|
||||
];
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 content-filter.example.com ESMTP\r\n');
|
||||
|
||||
let inData = false;
|
||||
let messageContent = '';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const text = data.toString();
|
||||
|
||||
if (inData) {
|
||||
messageContent += text;
|
||||
if (text.includes('\r\n.\r\n')) {
|
||||
inData = false;
|
||||
|
||||
// Calculate spam score
|
||||
let spamScore = 0;
|
||||
const lowerContent = messageContent.toLowerCase();
|
||||
|
||||
spamKeywords.forEach(({ word, score }) => {
|
||||
if (lowerContent.includes(word)) {
|
||||
spamScore += score;
|
||||
console.log(` [Server] Found spam keyword: "${word}" (+${score})`);
|
||||
}
|
||||
});
|
||||
|
||||
// Check for suspicious patterns
|
||||
if ((messageContent.match(/!/g) || []).length > 5) {
|
||||
spamScore += 2;
|
||||
console.log(' [Server] Excessive exclamation marks (+2)');
|
||||
}
|
||||
|
||||
if ((messageContent.match(/\$|€|£/g) || []).length > 3) {
|
||||
spamScore += 2;
|
||||
console.log(' [Server] Multiple currency symbols (+2)');
|
||||
}
|
||||
|
||||
if (messageContent.includes('ALL CAPS') || /[A-Z]{10,}/.test(messageContent)) {
|
||||
spamScore += 1;
|
||||
console.log(' [Server] Excessive capitals (+1)');
|
||||
}
|
||||
|
||||
console.log(` [Server] Total spam score: ${spamScore}`);
|
||||
|
||||
if (spamScore >= 10) {
|
||||
socket.write('550 5.7.1 Message rejected due to spam content\r\n');
|
||||
} else if (spamScore >= 5) {
|
||||
socket.write('250 OK: Message quarantined for review\r\n');
|
||||
} else {
|
||||
socket.write('250 OK: Message accepted\r\n');
|
||||
}
|
||||
|
||||
messageContent = '';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const command = text.trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-content-filter.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 1: Clean email
|
||||
const cleanEmail = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Business proposal',
|
||||
text: 'I would like to discuss our upcoming project. Please let me know your availability.'
|
||||
});
|
||||
|
||||
const cleanResult = await smtpClient.sendMail(cleanEmail);
|
||||
console.log(' Clean email: Accepted');
|
||||
expect(cleanResult.response).toContain('Message accepted');
|
||||
|
||||
// Test 2: Suspicious email
|
||||
const suspiciousEmail = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'You are a WINNER!',
|
||||
text: 'Click here to claim your lottery prize! Act now! 100% guarantee!'
|
||||
});
|
||||
|
||||
const suspiciousResult = await smtpClient.sendMail(suspiciousEmail);
|
||||
console.log(' Suspicious email: Quarantined');
|
||||
expect(suspiciousResult.response).toContain('quarantined');
|
||||
|
||||
// Test 3: Spam email
|
||||
const spamEmail = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'FREE MONEY - VIAGRA - LOTTERY WINNER!!!',
|
||||
text: 'CLICK HERE NOW!!! 100% FREE VIAGRA!!! You are a LOTTERY WINNER!!! Act now to claim your FREE MONEY!!! $$$€€€£££'
|
||||
});
|
||||
|
||||
try {
|
||||
await smtpClient.sendMail(spamEmail);
|
||||
console.log(' Unexpected: Spam email accepted');
|
||||
} catch (error) {
|
||||
console.log(' Spam email: Rejected');
|
||||
expect(error.message).toContain('spam content');
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 3: Greylisting
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing greylisting`);
|
||||
|
||||
const greylist = new Map<string, { firstSeen: number; attempts: number }>();
|
||||
const greylistDuration = 2000; // 2 seconds for testing
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 greylist.example.com ESMTP\r\n');
|
||||
|
||||
let triplet = '';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-greylist.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
const from = command.match(/<(.+)>/)?.[1] || '';
|
||||
triplet = `${socket.remoteAddress}-${from}`;
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
const to = command.match(/<(.+)>/)?.[1] || '';
|
||||
triplet += `-${to}`;
|
||||
|
||||
const now = Date.now();
|
||||
const greylistEntry = greylist.get(triplet);
|
||||
|
||||
if (!greylistEntry) {
|
||||
// First time seeing this triplet
|
||||
greylist.set(triplet, { firstSeen: now, attempts: 1 });
|
||||
console.log(' [Server] New sender - greylisting');
|
||||
socket.write('451 4.7.1 Greylisting in effect, please retry later\r\n');
|
||||
} else {
|
||||
greylistEntry.attempts++;
|
||||
const elapsed = now - greylistEntry.firstSeen;
|
||||
|
||||
if (elapsed < greylistDuration) {
|
||||
console.log(` [Server] Too soon (${elapsed}ms) - still greylisted`);
|
||||
socket.write('451 4.7.1 Greylisting in effect, please retry later\r\n');
|
||||
} else {
|
||||
console.log(` [Server] Greylist passed (${greylistEntry.attempts} attempts)`);
|
||||
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 accepted after greylisting\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: 'Greylist test',
|
||||
text: 'Testing greylisting mechanism'
|
||||
});
|
||||
|
||||
// First attempt - should be greylisted
|
||||
try {
|
||||
await smtpClient.sendMail(email);
|
||||
console.log(' Unexpected: First attempt succeeded');
|
||||
} catch (error) {
|
||||
console.log(' First attempt: Greylisted as expected');
|
||||
expect(error.message).toContain('Greylisting');
|
||||
}
|
||||
|
||||
// Wait and retry
|
||||
console.log(` Waiting ${greylistDuration}ms before retry...`);
|
||||
await new Promise(resolve => setTimeout(resolve, greylistDuration + 100));
|
||||
|
||||
// Second attempt - should succeed
|
||||
const retryResult = await smtpClient.sendMail(email);
|
||||
console.log(' Retry attempt: Accepted after greylist period');
|
||||
expect(retryResult).toBeDefined();
|
||||
expect(retryResult.response).toContain('after greylisting');
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 4: DNS blacklist (DNSBL) checking
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing DNSBL checking`);
|
||||
|
||||
const blacklistedIPs = ['192.168.1.100', '10.0.0.50'];
|
||||
const blacklistedDomains = ['spam-domain.com', 'phishing-site.net'];
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
const clientIP = socket.remoteAddress || '';
|
||||
console.log(` [Server] Client connected from ${clientIP}`);
|
||||
|
||||
// Simulate DNSBL check
|
||||
const isBlacklisted = blacklistedIPs.some(ip => clientIP.includes(ip));
|
||||
|
||||
if (isBlacklisted) {
|
||||
console.log(' [Server] IP found in DNSBL');
|
||||
socket.write('554 5.7.1 Your IP is listed in DNSBL\r\n');
|
||||
socket.end();
|
||||
return;
|
||||
}
|
||||
|
||||
socket.write('220 dnsbl.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
const domain = command.split(' ')[1];
|
||||
if (blacklistedDomains.includes(domain)) {
|
||||
console.log(' [Server] HELO domain in DNSBL');
|
||||
socket.write('554 5.7.1 Your domain is blacklisted\r\n');
|
||||
socket.end();
|
||||
return;
|
||||
}
|
||||
socket.write('250-dnsbl.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
const fromAddress = command.match(/<(.+)>/)?.[1] || '';
|
||||
const fromDomain = fromAddress.split('@')[1];
|
||||
|
||||
if (blacklistedDomains.includes(fromDomain)) {
|
||||
console.log(' [Server] Sender domain in DNSBL');
|
||||
socket.write('554 5.7.1 Sender domain is blacklisted\r\n');
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Test with clean sender
|
||||
const cleanEmail = new plugins.smartmail.Email({
|
||||
from: 'sender@clean-domain.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'DNSBL test',
|
||||
text: 'Testing DNSBL checking'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(cleanEmail);
|
||||
console.log(' Clean sender: Accepted');
|
||||
expect(result).toBeDefined();
|
||||
|
||||
// Test with blacklisted domain
|
||||
const blacklistedEmail = new plugins.smartmail.Email({
|
||||
from: 'sender@spam-domain.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Blacklisted domain test',
|
||||
text: 'Testing from blacklisted domain'
|
||||
});
|
||||
|
||||
try {
|
||||
await smtpClient.sendMail(blacklistedEmail);
|
||||
console.log(' Unexpected: Blacklisted domain accepted');
|
||||
} catch (error) {
|
||||
console.log(' Blacklisted domain: Rejected');
|
||||
expect(error.message).toContain('blacklisted');
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 5: Connection behavior analysis
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing connection behavior analysis`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
|
||||
const connectionStart = Date.now();
|
||||
let commandCount = 0;
|
||||
let errorCount = 0;
|
||||
let rapidCommands = 0;
|
||||
let lastCommandTime = Date.now();
|
||||
|
||||
// Set initial timeout
|
||||
socket.setTimeout(30000); // 30 seconds
|
||||
|
||||
socket.write('220 behavior.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const now = Date.now();
|
||||
const timeSinceLastCommand = now - lastCommandTime;
|
||||
lastCommandTime = now;
|
||||
|
||||
commandCount++;
|
||||
|
||||
// Check for rapid-fire commands (bot behavior)
|
||||
if (timeSinceLastCommand < 50) {
|
||||
rapidCommands++;
|
||||
if (rapidCommands > 5) {
|
||||
console.log(' [Server] Detected rapid-fire commands (bot behavior)');
|
||||
socket.write('421 4.7.0 Suspicious behavior detected\r\n');
|
||||
socket.end();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
rapidCommands = 0; // Reset counter
|
||||
}
|
||||
|
||||
const command = data.toString().trim();
|
||||
console.log(` [Server] Received: ${command} (${timeSinceLastCommand}ms since last)`);
|
||||
|
||||
// Check for invalid commands (spam bot behavior)
|
||||
if (!command.match(/^(EHLO|HELO|MAIL FROM:|RCPT TO:|DATA|QUIT|RSET|NOOP|AUTH|\.)/i)) {
|
||||
errorCount++;
|
||||
if (errorCount > 3) {
|
||||
console.log(' [Server] Too many invalid commands');
|
||||
socket.write('421 4.7.0 Too many errors\r\n');
|
||||
socket.end();
|
||||
return;
|
||||
}
|
||||
socket.write('500 5.5.1 Command not recognized\r\n');
|
||||
return;
|
||||
}
|
||||
|
||||
if (command.startsWith('EHLO') || command.startsWith('HELO')) {
|
||||
socket.write('250-behavior.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 === '.') {
|
||||
const connectionDuration = Date.now() - connectionStart;
|
||||
console.log(` [Server] Session duration: ${connectionDuration}ms, commands: ${commandCount}`);
|
||||
socket.write('250 OK: Message accepted\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
} else if (command === 'NOOP') {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('timeout', () => {
|
||||
console.log(' [Server] Connection timeout - possible spam bot');
|
||||
socket.write('421 4.4.2 Connection timeout\r\n');
|
||||
socket.end();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Test normal behavior
|
||||
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: 'Behavior test',
|
||||
text: 'Testing normal email sending behavior'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(' Normal behavior: Accepted');
|
||||
expect(result).toBeDefined();
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 6: Attachment and link scanning
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing attachment and link scanning`);
|
||||
|
||||
const dangerousExtensions = ['.exe', '.scr', '.vbs', '.com', '.bat', '.cmd', '.pif'];
|
||||
const suspiciousLinks = ['bit.ly', 'tinyurl.com', 'short.link'];
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 scanner.example.com ESMTP\r\n');
|
||||
|
||||
let inData = false;
|
||||
let messageContent = '';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const text = data.toString();
|
||||
|
||||
if (inData) {
|
||||
messageContent += text;
|
||||
if (text.includes('\r\n.\r\n')) {
|
||||
inData = false;
|
||||
|
||||
let threatLevel = 0;
|
||||
const threats: string[] = [];
|
||||
|
||||
// Check for dangerous attachments
|
||||
const attachmentMatch = messageContent.match(/filename="([^"]+)"/gi);
|
||||
if (attachmentMatch) {
|
||||
attachmentMatch.forEach(match => {
|
||||
const filename = match.match(/filename="([^"]+)"/i)?.[1] || '';
|
||||
const extension = filename.substring(filename.lastIndexOf('.')).toLowerCase();
|
||||
|
||||
if (dangerousExtensions.includes(extension)) {
|
||||
threatLevel += 10;
|
||||
threats.push(`Dangerous attachment: ${filename}`);
|
||||
console.log(` [Server] Found dangerous attachment: ${filename}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check for suspicious links
|
||||
const urlMatch = messageContent.match(/https?:\/\/[^\s]+/gi);
|
||||
if (urlMatch) {
|
||||
urlMatch.forEach(url => {
|
||||
if (suspiciousLinks.some(domain => url.includes(domain))) {
|
||||
threatLevel += 5;
|
||||
threats.push(`Suspicious link: ${url}`);
|
||||
console.log(` [Server] Found suspicious link: ${url}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check for phishing patterns
|
||||
if (messageContent.includes('verify your account') && urlMatch) {
|
||||
threatLevel += 5;
|
||||
threats.push('Possible phishing attempt');
|
||||
}
|
||||
|
||||
console.log(` [Server] Threat level: ${threatLevel}`);
|
||||
|
||||
if (threatLevel >= 10) {
|
||||
socket.write(`550 5.7.1 Message rejected: ${threats.join(', ')}\r\n`);
|
||||
} else if (threatLevel >= 5) {
|
||||
socket.write('250 OK: Message flagged for review\r\n');
|
||||
} else {
|
||||
socket.write('250 OK: Message scanned and accepted\r\n');
|
||||
}
|
||||
|
||||
messageContent = '';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const command = text.trim();
|
||||
console.log(` [Server] Received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-scanner.example.com\r\n');
|
||||
socket.write('250-SIZE 10485760\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 1: Clean email with safe attachment
|
||||
const safeEmail = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Document for review',
|
||||
text: 'Please find the attached document.',
|
||||
attachments: [{
|
||||
filename: 'report.pdf',
|
||||
content: 'PDF content here'
|
||||
}]
|
||||
});
|
||||
|
||||
const safeResult = await smtpClient.sendMail(safeEmail);
|
||||
console.log(' Safe email: Scanned and accepted');
|
||||
expect(safeResult.response).toContain('scanned and accepted');
|
||||
|
||||
// Test 2: Email with suspicious link
|
||||
const suspiciousEmail = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Check this out',
|
||||
text: 'Click here: https://bit.ly/abc123 to verify your account',
|
||||
html: '<p>Click <a href="https://bit.ly/abc123">here</a> to verify your account</p>'
|
||||
});
|
||||
|
||||
const suspiciousResult = await smtpClient.sendMail(suspiciousEmail);
|
||||
console.log(' Suspicious email: Flagged for review');
|
||||
expect(suspiciousResult.response).toContain('flagged for review');
|
||||
|
||||
// Test 3: Email with dangerous attachment
|
||||
const dangerousEmail = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Important update',
|
||||
text: 'Please run the attached file',
|
||||
attachments: [{
|
||||
filename: 'update.exe',
|
||||
content: Buffer.from('MZ\x90\x00\x03') // Fake executable header
|
||||
}]
|
||||
});
|
||||
|
||||
try {
|
||||
await smtpClient.sendMail(dangerousEmail);
|
||||
console.log(' Unexpected: Dangerous attachment accepted');
|
||||
} catch (error) {
|
||||
console.log(' Dangerous attachment: Rejected');
|
||||
expect(error.message).toContain('Dangerous attachment');
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
console.log(`\n${testId}: All ${scenarioCount} anti-spam scenarios tested ✓`);
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user