493 lines
16 KiB
TypeScript
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(); |