399 lines
12 KiB
TypeScript
399 lines
12 KiB
TypeScript
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
import * as net from 'net';
|
|
import { startTestServer, stopTestServer } from '../../helpers/server.loader.js';
|
|
|
|
// Test configuration
|
|
const TEST_PORT = 2525;
|
|
|
|
let testServer;
|
|
const TEST_TIMEOUT = 10000;
|
|
|
|
// Setup
|
|
tap.test('prepare server', async () => {
|
|
testServer = await startTestServer({ port: TEST_PORT });
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
});
|
|
|
|
// Test: Basic RSET command
|
|
tap.test('RSET - should reset transaction after MAIL FROM', 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')) {
|
|
currentStep = 'rset';
|
|
socket.write('RSET\r\n');
|
|
} else if (currentStep === 'rset' && receivedData.includes('250')) {
|
|
// RSET successful, try to send MAIL FROM again to verify reset
|
|
currentStep = 'mail_from_after_rset';
|
|
socket.write('MAIL FROM:<newsender@example.com>\r\n');
|
|
} else if (currentStep === 'mail_from_after_rset' && receivedData.includes('250')) {
|
|
socket.write('QUIT\r\n');
|
|
setTimeout(() => {
|
|
socket.destroy();
|
|
expect(receivedData).toInclude('250 OK'); // RSET response
|
|
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: RSET after RCPT TO
|
|
tap.test('RSET - should reset transaction after RCPT TO', 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')) {
|
|
currentStep = 'rcpt_to';
|
|
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
|
} else if (currentStep === 'rcpt_to' && receivedData.includes('250')) {
|
|
currentStep = 'rset';
|
|
socket.write('RSET\r\n');
|
|
} else if (currentStep === 'rset' && receivedData.includes('250')) {
|
|
// After RSET, should need MAIL FROM before RCPT TO
|
|
currentStep = 'rcpt_to_after_rset';
|
|
socket.write('RCPT TO:<newrecipient@example.com>\r\n');
|
|
} else if (currentStep === 'rcpt_to_after_rset' && receivedData.includes('503')) {
|
|
// Should get 503 bad sequence
|
|
socket.write('QUIT\r\n');
|
|
setTimeout(() => {
|
|
socket.destroy();
|
|
expect(receivedData).toInclude('503'); // Bad sequence after RSET
|
|
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: RSET during DATA
|
|
tap.test('RSET - should reset transaction during DATA phase', 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')) {
|
|
currentStep = 'rcpt_to';
|
|
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
|
} else if (currentStep === 'rcpt_to' && receivedData.includes('250')) {
|
|
currentStep = 'data';
|
|
socket.write('DATA\r\n');
|
|
} else if (currentStep === 'data' && receivedData.includes('354')) {
|
|
// Start sending data but then RSET
|
|
currentStep = 'rset_during_data';
|
|
socket.write('Subject: Test\r\n\r\nPartial message...\r\n');
|
|
socket.write('RSET\r\n'); // This should be treated as part of data
|
|
socket.write('\r\n.\r\n'); // End data
|
|
} else if (currentStep === 'rset_during_data' && receivedData.includes('250')) {
|
|
// Message accepted, now send actual RSET
|
|
currentStep = 'rset_after_data';
|
|
socket.write('RSET\r\n');
|
|
} else if (currentStep === 'rset_after_data' && receivedData.includes('250')) {
|
|
socket.write('QUIT\r\n');
|
|
setTimeout(() => {
|
|
socket.destroy();
|
|
expect(receivedData).toInclude('250');
|
|
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: Multiple RSET commands
|
|
tap.test('RSET - should handle multiple consecutive RSET commands', async (tools) => {
|
|
const done = tools.defer();
|
|
|
|
const socket = net.createConnection({
|
|
host: 'localhost',
|
|
port: TEST_PORT,
|
|
timeout: TEST_TIMEOUT
|
|
});
|
|
|
|
let receivedData = '';
|
|
let currentStep = 'connecting';
|
|
let rsetCount = 0;
|
|
const maxRsets = 3;
|
|
|
|
socket.on('data', (data) => {
|
|
receivedData += data.toString();
|
|
|
|
if (currentStep === 'connecting' && receivedData.includes('220')) {
|
|
currentStep = 'ehlo';
|
|
receivedData = '';
|
|
socket.write('EHLO test.example.com\r\n');
|
|
} else if (currentStep === 'ehlo' && receivedData.includes('250 ')) {
|
|
currentStep = 'mail_from';
|
|
receivedData = '';
|
|
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
|
} else if (currentStep === 'mail_from' && receivedData.includes('250')) {
|
|
currentStep = 'multiple_rsets';
|
|
receivedData = '';
|
|
socket.write('RSET\r\n');
|
|
} else if (currentStep === 'multiple_rsets' && receivedData.includes('250')) {
|
|
rsetCount++;
|
|
receivedData = ''; // Clear buffer after processing
|
|
|
|
if (rsetCount < maxRsets) {
|
|
socket.write('RSET\r\n');
|
|
} else {
|
|
socket.write('QUIT\r\n');
|
|
setTimeout(() => {
|
|
socket.destroy();
|
|
expect(rsetCount).toEqual(maxRsets);
|
|
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: RSET without transaction
|
|
tap.test('RSET - should work without active transaction', 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 = 'rset_without_transaction';
|
|
socket.write('RSET\r\n');
|
|
} else if (currentStep === 'rset_without_transaction' && receivedData.includes('250')) {
|
|
socket.write('QUIT\r\n');
|
|
setTimeout(() => {
|
|
socket.destroy();
|
|
expect(receivedData).toInclude('250'); // RSET should work even without transaction
|
|
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: RSET with multiple recipients
|
|
tap.test('RSET - should clear all 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;
|
|
|
|
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 = 'add_recipients';
|
|
recipientCount++;
|
|
socket.write(`RCPT TO:<recipient${recipientCount}@example.com>\r\n`);
|
|
} else if (currentStep === 'add_recipients' && receivedData.includes('250')) {
|
|
if (recipientCount < 3) {
|
|
recipientCount++;
|
|
receivedData = ''; // Clear buffer
|
|
socket.write(`RCPT TO:<recipient${recipientCount}@example.com>\r\n`);
|
|
} else {
|
|
currentStep = 'rset';
|
|
socket.write('RSET\r\n');
|
|
}
|
|
} else if (currentStep === 'rset' && receivedData.includes('250')) {
|
|
// After RSET, all recipients should be cleared
|
|
currentStep = 'data_after_rset';
|
|
socket.write('DATA\r\n');
|
|
} else if (currentStep === 'data_after_rset' && receivedData.includes('503')) {
|
|
// Should get 503 bad sequence (no recipients)
|
|
socket.write('QUIT\r\n');
|
|
setTimeout(() => {
|
|
socket.destroy();
|
|
expect(receivedData).toInclude('503');
|
|
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: RSET with parameter (should be ignored)
|
|
tap.test('RSET - should ignore parameters', 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 = 'rset_with_param';
|
|
socket.write('RSET ignored parameter\r\n'); // Parameters should be ignored
|
|
} else if (currentStep === 'rset_with_param' && receivedData.includes('250')) {
|
|
socket.write('QUIT\r\n');
|
|
setTimeout(() => {
|
|
socket.destroy();
|
|
expect(receivedData).toInclude('250');
|
|
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('cleanup server', async () => {
|
|
await stopTestServer(testServer);
|
|
});
|
|
|
|
// Start the test
|
|
export default tap.start(); |