476 lines
15 KiB
TypeScript
476 lines
15 KiB
TypeScript
|
import { tap, expect } from '@push.rocks/tapbundle';
|
||
|
import * as net from 'net';
|
||
|
import * as path from 'path';
|
||
|
import { startTestServer, stopTestServer } from '../server.loader.js';
|
||
|
|
||
|
// Test configuration
|
||
|
const TEST_PORT = 2525;
|
||
|
const TEST_TIMEOUT = 15000;
|
||
|
|
||
|
let testServer: any;
|
||
|
|
||
|
// Setup
|
||
|
tap.test('setup - start SMTP server', async () => {
|
||
|
testServer = await startTestServer();
|
||
|
|
||
|
expect(testServer).toBeTypeofObject();
|
||
|
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 OK')) {
|
||
|
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' && receivedData.includes('503')) {
|
||
|
socket.write('QUIT\r\n');
|
||
|
setTimeout(() => {
|
||
|
socket.destroy();
|
||
|
expect(receivedData).toInclude('503'); // Bad sequence
|
||
|
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();
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// Start the test
|
||
|
tap.start();
|