update
This commit is contained in:
@ -0,0 +1,493 @@
|
||||
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
|
||||
tap.start();
|
Reference in New Issue
Block a user