dcrouter/test/suite/smtpserver_email-processing/test.ep-03.multiple-recipients.ts
2025-05-25 19:05:43 +00:00

493 lines
16 KiB
TypeScript

import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as net from 'net';
import * as path from 'path';
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
// Test configuration
const TEST_PORT = 30049;
const TEST_TIMEOUT = 15000;
let testServer: ITestServer;
// Setup
tap.test('setup - start SMTP server', async () => {
testServer = await startTestServer({ port: TEST_PORT, hostname: 'localhost' });
expect(testServer).toBeDefined();
expect(testServer.port).toEqual(TEST_PORT);
});
// Test: Basic multiple recipients
tap.test('Multiple Recipients - should accept multiple valid recipients', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: TEST_TIMEOUT
});
let receivedData = '';
let currentStep = 'connecting';
let recipientCount = 0;
const recipients = [
'recipient1@example.com',
'recipient2@example.com',
'recipient3@example.com'
];
let acceptedRecipients = 0;
socket.on('data', (data) => {
receivedData += data.toString();
if (currentStep === 'connecting' && receivedData.includes('220')) {
currentStep = 'ehlo';
socket.write('EHLO test.example.com\r\n');
} else if (currentStep === 'ehlo' && receivedData.includes('250')) {
currentStep = 'mail_from';
socket.write('MAIL FROM:<sender@example.com>\r\n');
} else if (currentStep === 'mail_from' && receivedData.includes('250')) {
currentStep = 'rcpt_to';
socket.write(`RCPT TO:<${recipients[recipientCount]}>\r\n`);
} else if (currentStep === 'rcpt_to') {
if (receivedData.includes('250')) {
acceptedRecipients++;
recipientCount++;
if (recipientCount < recipients.length) {
receivedData = ''; // Clear buffer for next response
socket.write(`RCPT TO:<${recipients[recipientCount]}>\r\n`);
} else {
currentStep = 'data';
socket.write('DATA\r\n');
}
}
} else if (currentStep === 'data' && receivedData.includes('354')) {
currentStep = 'email_content';
const emailContent = `Subject: Multiple Recipients Test\r\nFrom: sender@example.com\r\nTo: ${recipients.join(', ')}\r\n\r\nThis email was sent to ${acceptedRecipients} recipients.\r\n`;
socket.write(emailContent);
socket.write('\r\n.\r\n');
} else if (currentStep === 'email_content' && receivedData.includes('250')) {
socket.write('QUIT\r\n');
setTimeout(() => {
socket.destroy();
expect(acceptedRecipients).toEqual(recipients.length);
done.resolve();
}, 100);
}
});
socket.on('error', (error) => {
done.reject(error);
});
socket.on('timeout', () => {
socket.destroy();
done.reject(new Error(`Connection timeout at step: ${currentStep}`));
});
await done.promise;
});
// Test: Mixed valid and invalid recipients
tap.test('Multiple Recipients - should handle mix of valid and invalid recipients', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: TEST_TIMEOUT
});
let receivedData = '';
let currentStep = 'connecting';
let recipientIndex = 0;
const recipients = [
'valid@example.com',
'invalid-email', // Invalid format
'another.valid@example.com',
'@example.com', // Invalid format
'third.valid@example.com'
];
const recipientResults: Array<{ email: string, accepted: boolean }> = [];
socket.on('data', (data) => {
receivedData += data.toString();
if (currentStep === 'connecting' && receivedData.includes('220')) {
currentStep = 'ehlo';
socket.write('EHLO test.example.com\r\n');
} else if (currentStep === 'ehlo' && receivedData.includes('250')) {
currentStep = 'mail_from';
socket.write('MAIL FROM:<sender@example.com>\r\n');
} else if (currentStep === 'mail_from' && receivedData.includes('250')) {
currentStep = 'rcpt_to';
socket.write(`RCPT TO:<${recipients[recipientIndex]}>\r\n`);
} else if (currentStep === 'rcpt_to') {
const lines = receivedData.split('\r\n');
const lastLine = lines[lines.length - 2] || lines[lines.length - 1];
if (lastLine.match(/^\d{3}/)) {
const accepted = lastLine.startsWith('250');
recipientResults.push({
email: recipients[recipientIndex],
accepted: accepted
});
recipientIndex++;
if (recipientIndex < recipients.length) {
receivedData = ''; // Clear buffer
socket.write(`RCPT TO:<${recipients[recipientIndex]}>\r\n`);
} else {
const acceptedCount = recipientResults.filter(r => r.accepted).length;
if (acceptedCount > 0) {
currentStep = 'data';
socket.write('DATA\r\n');
} else {
socket.write('QUIT\r\n');
setTimeout(() => {
socket.destroy();
expect(acceptedCount).toEqual(0);
done.resolve();
}, 100);
}
}
}
} else if (currentStep === 'data' && receivedData.includes('354')) {
currentStep = 'email_content';
const acceptedEmails = recipientResults.filter(r => r.accepted).map(r => r.email);
const emailContent = `Subject: Mixed Recipients Test\r\nFrom: sender@example.com\r\nTo: ${acceptedEmails.join(', ')}\r\n\r\nDelivered to ${acceptedEmails.length} valid recipients.\r\n`;
socket.write(emailContent);
socket.write('\r\n.\r\n');
} else if (currentStep === 'email_content' && receivedData.includes('250')) {
socket.write('QUIT\r\n');
setTimeout(() => {
socket.destroy();
const acceptedCount = recipientResults.filter(r => r.accepted).length;
const rejectedCount = recipientResults.filter(r => !r.accepted).length;
expect(acceptedCount).toEqual(3); // 3 valid recipients
expect(rejectedCount).toEqual(2); // 2 invalid recipients
done.resolve();
}, 100);
}
});
socket.on('error', (error) => {
done.reject(error);
});
socket.on('timeout', () => {
socket.destroy();
done.reject(new Error(`Connection timeout at step: ${currentStep}`));
});
await done.promise;
});
// Test: Large number of recipients
tap.test('Multiple Recipients - should handle many recipients', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: TEST_TIMEOUT
});
let receivedData = '';
let currentStep = 'connecting';
let recipientCount = 0;
const totalRecipients = 10;
const recipients: string[] = [];
for (let i = 1; i <= totalRecipients; i++) {
recipients.push(`recipient${i}@example.com`);
}
let acceptedCount = 0;
socket.on('data', (data) => {
receivedData += data.toString();
if (currentStep === 'connecting' && receivedData.includes('220')) {
currentStep = 'ehlo';
socket.write('EHLO test.example.com\r\n');
} else if (currentStep === 'ehlo' && receivedData.includes('250')) {
currentStep = 'mail_from';
socket.write('MAIL FROM:<sender@example.com>\r\n');
} else if (currentStep === 'mail_from' && receivedData.includes('250')) {
currentStep = 'rcpt_to';
socket.write(`RCPT TO:<${recipients[recipientCount]}>\r\n`);
} else if (currentStep === 'rcpt_to') {
if (receivedData.includes('250')) {
acceptedCount++;
}
recipientCount++;
if (recipientCount < recipients.length) {
receivedData = ''; // Clear buffer
socket.write(`RCPT TO:<${recipients[recipientCount]}>\r\n`);
} else {
currentStep = 'data';
socket.write('DATA\r\n');
}
} else if (currentStep === 'data' && receivedData.includes('354')) {
currentStep = 'email_content';
const emailContent = `Subject: Large Recipients Test\r\nFrom: sender@example.com\r\n\r\nSent to ${acceptedCount} recipients.\r\n`;
socket.write(emailContent);
socket.write('\r\n.\r\n');
} else if (currentStep === 'email_content' && receivedData.includes('250')) {
socket.write('QUIT\r\n');
setTimeout(() => {
socket.destroy();
expect(acceptedCount).toBeGreaterThan(0);
expect(acceptedCount).toBeLessThan(totalRecipients + 1);
done.resolve();
}, 100);
}
});
socket.on('error', (error) => {
done.reject(error);
});
socket.on('timeout', () => {
socket.destroy();
done.reject(new Error(`Connection timeout at step: ${currentStep}`));
});
await done.promise;
});
// Test: Duplicate recipients
tap.test('Multiple Recipients - should handle duplicate recipients', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: TEST_TIMEOUT
});
let receivedData = '';
let currentStep = 'connecting';
let recipientCount = 0;
const recipients = [
'duplicate@example.com',
'unique@example.com',
'duplicate@example.com', // Duplicate
'another@example.com',
'duplicate@example.com' // Another duplicate
];
const results: boolean[] = [];
socket.on('data', (data) => {
receivedData += data.toString();
if (currentStep === 'connecting' && receivedData.includes('220')) {
currentStep = 'ehlo';
socket.write('EHLO test.example.com\r\n');
} else if (currentStep === 'ehlo' && receivedData.includes('250')) {
currentStep = 'mail_from';
socket.write('MAIL FROM:<sender@example.com>\r\n');
} else if (currentStep === 'mail_from' && receivedData.includes('250')) {
currentStep = 'rcpt_to';
socket.write(`RCPT TO:<${recipients[recipientCount]}>\r\n`);
} else if (currentStep === 'rcpt_to') {
if (receivedData.match(/[245]\d{2}/)) {
results.push(receivedData.includes('250'));
recipientCount++;
if (recipientCount < recipients.length) {
receivedData = ''; // Clear buffer
socket.write(`RCPT TO:<${recipients[recipientCount]}>\r\n`);
} else {
currentStep = 'data';
socket.write('DATA\r\n');
}
}
} else if (currentStep === 'data' && receivedData.includes('354')) {
currentStep = 'email_content';
const emailContent = `Subject: Duplicate Recipients Test\r\nFrom: sender@example.com\r\n\r\nTesting duplicate recipient handling.\r\n`;
socket.write(emailContent);
socket.write('\r\n.\r\n');
} else if (currentStep === 'email_content' && receivedData.includes('250')) {
socket.write('QUIT\r\n');
setTimeout(() => {
socket.destroy();
expect(results.length).toEqual(recipients.length);
done.resolve();
}, 100);
}
});
socket.on('error', (error) => {
done.reject(error);
});
socket.on('timeout', () => {
socket.destroy();
done.reject(new Error(`Connection timeout at step: ${currentStep}`));
});
await done.promise;
});
// Test: No recipients (should fail DATA)
tap.test('Multiple Recipients - DATA should fail with no recipients', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: TEST_TIMEOUT
});
let receivedData = '';
let currentStep = 'connecting';
socket.on('data', (data) => {
receivedData += data.toString();
if (currentStep === 'connecting' && receivedData.includes('220')) {
currentStep = 'ehlo';
socket.write('EHLO test.example.com\r\n');
} else if (currentStep === 'ehlo' && receivedData.includes('250')) {
currentStep = 'mail_from';
socket.write('MAIL FROM:<sender@example.com>\r\n');
} else if (currentStep === 'mail_from' && receivedData.includes('250')) {
// Skip RCPT TO, go directly to DATA
currentStep = 'data_no_recipients';
socket.write('DATA\r\n');
} else if (currentStep === 'data_no_recipients') {
if (receivedData.includes('503')) {
// Expected: bad sequence error
socket.write('QUIT\r\n');
setTimeout(() => {
socket.destroy();
expect(receivedData).toInclude('503'); // Bad sequence
done.resolve();
}, 100);
} else if (receivedData.includes('354')) {
// Some servers accept DATA without recipients and fail later
// Send empty data to trigger the error
socket.write('.\r\n');
currentStep = 'data_sent';
}
} else if (currentStep === 'data_sent' && receivedData.match(/[45]\d{2}/)) {
socket.write('QUIT\r\n');
setTimeout(() => {
socket.destroy();
// Should get an error when trying to send without recipients
expect(receivedData).toMatch(/[45]\d{2}/);
done.resolve();
}, 100);
}
});
socket.on('error', (error) => {
done.reject(error);
});
socket.on('timeout', () => {
socket.destroy();
done.reject(new Error(`Connection timeout at step: ${currentStep}`));
});
await done.promise;
});
// Test: Recipients with different domains
tap.test('Multiple Recipients - should handle recipients from different domains', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: TEST_TIMEOUT
});
let receivedData = '';
let currentStep = 'connecting';
let recipientCount = 0;
const recipients = [
'user1@example.com',
'user2@test.com',
'user3@localhost',
'user4@example.org',
'user5@subdomain.example.com'
];
let acceptedCount = 0;
socket.on('data', (data) => {
receivedData += data.toString();
if (currentStep === 'connecting' && receivedData.includes('220')) {
currentStep = 'ehlo';
socket.write('EHLO test.example.com\r\n');
} else if (currentStep === 'ehlo' && receivedData.includes('250')) {
currentStep = 'mail_from';
socket.write('MAIL FROM:<sender@example.com>\r\n');
} else if (currentStep === 'mail_from' && receivedData.includes('250')) {
currentStep = 'rcpt_to';
socket.write(`RCPT TO:<${recipients[recipientCount]}>\r\n`);
} else if (currentStep === 'rcpt_to') {
if (receivedData.includes('250')) {
acceptedCount++;
}
recipientCount++;
if (recipientCount < recipients.length) {
receivedData = ''; // Clear buffer
socket.write(`RCPT TO:<${recipients[recipientCount]}>\r\n`);
} else {
if (acceptedCount > 0) {
currentStep = 'data';
socket.write('DATA\r\n');
} else {
socket.write('QUIT\r\n');
setTimeout(() => {
socket.destroy();
done.resolve();
}, 100);
}
}
} else if (currentStep === 'data' && receivedData.includes('354')) {
currentStep = 'email_content';
const emailContent = `Subject: Multi-domain Test\r\nFrom: sender@example.com\r\n\r\nDelivered to ${acceptedCount} recipients across different domains.\r\n`;
socket.write(emailContent);
socket.write('\r\n.\r\n');
} else if (currentStep === 'email_content' && receivedData.includes('250')) {
socket.write('QUIT\r\n');
setTimeout(() => {
socket.destroy();
expect(acceptedCount).toBeGreaterThan(0);
done.resolve();
}, 100);
}
});
socket.on('error', (error) => {
done.reject(error);
});
socket.on('timeout', () => {
socket.destroy();
done.reject(new Error(`Connection timeout at step: ${currentStep}`));
});
await done.promise;
});
// Teardown
tap.test('teardown - stop SMTP server', async () => {
if (testServer) {
await stopTestServer(testServer);
}
expect(true).toEqual(true);
});
// Start the test
export default tap.start();