425 lines
12 KiB
TypeScript
425 lines
12 KiB
TypeScript
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
import * as net from 'net';
|
|
import * as path from 'path';
|
|
import { startTestServer, stopTestServer } from '../../helpers/server.loader.js';
|
|
import type { ITestServer } from '../../helpers/server.loader.js';
|
|
|
|
// Test configuration
|
|
const TEST_PORT = 30051;
|
|
const TEST_TIMEOUT = 10000;
|
|
|
|
let testServer: ITestServer;
|
|
|
|
// Setup
|
|
tap.test('setup - start SMTP server', async () => {
|
|
testServer = await startTestServer({
|
|
port: TEST_PORT,
|
|
tlsEnabled: false,
|
|
hostname: 'localhost'
|
|
});
|
|
|
|
expect(testServer).toBeDefined();
|
|
expect(testServer.port).toEqual(TEST_PORT);
|
|
});
|
|
|
|
// Test: MAIL FROM before EHLO/HELO
|
|
tap.test('Invalid Sequence - should reject MAIL FROM before EHLO', 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 = 'mail_from_without_ehlo';
|
|
socket.write('MAIL FROM:<test@example.com>\r\n');
|
|
} else if (currentStep === 'mail_from_without_ehlo' && receivedData.includes('503')) {
|
|
socket.write('QUIT\r\n');
|
|
setTimeout(() => {
|
|
socket.destroy();
|
|
expect(receivedData).toInclude('503'); // Bad sequence of commands
|
|
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: RCPT TO before MAIL FROM
|
|
tap.test('Invalid Sequence - should reject RCPT TO before 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 = 'rcpt_without_mail';
|
|
socket.write('RCPT TO:<test@example.com>\r\n');
|
|
} else if (currentStep === 'rcpt_without_mail' && receivedData.includes('503')) {
|
|
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: DATA before RCPT TO
|
|
tap.test('Invalid Sequence - should reject DATA before 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:<test@example.com>\r\n');
|
|
} else if (currentStep === 'mail_from' && receivedData.includes('250')) {
|
|
currentStep = 'data_without_rcpt';
|
|
socket.write('DATA\r\n');
|
|
} else if (currentStep === 'data_without_rcpt' && receivedData.includes('503')) {
|
|
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: Multiple EHLO commands (should be allowed)
|
|
tap.test('Invalid Sequence - should allow multiple EHLO 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 ehloCount = 0;
|
|
|
|
socket.on('data', (data) => {
|
|
receivedData += data.toString();
|
|
|
|
if (currentStep === 'connecting' && receivedData.includes('220')) {
|
|
currentStep = 'first_ehlo';
|
|
socket.write('EHLO test1.example.com\r\n');
|
|
} else if (currentStep === 'first_ehlo' && receivedData.includes('250')) {
|
|
ehloCount++;
|
|
currentStep = 'second_ehlo';
|
|
receivedData = ''; // Clear buffer
|
|
socket.write('EHLO test2.example.com\r\n');
|
|
} else if (currentStep === 'second_ehlo' && receivedData.includes('250')) {
|
|
ehloCount++;
|
|
currentStep = 'third_ehlo';
|
|
receivedData = ''; // Clear buffer
|
|
socket.write('EHLO test3.example.com\r\n');
|
|
} else if (currentStep === 'third_ehlo' && receivedData.includes('250')) {
|
|
ehloCount++;
|
|
socket.write('QUIT\r\n');
|
|
setTimeout(() => {
|
|
socket.destroy();
|
|
expect(ehloCount).toEqual(3); // All EHLO commands should succeed
|
|
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 MAIL FROM without RSET
|
|
tap.test('Invalid Sequence - should reject second MAIL FROM without RSET', 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 = 'first_mail_from';
|
|
socket.write('MAIL FROM:<sender1@example.com>\r\n');
|
|
} else if (currentStep === 'first_mail_from' && receivedData.includes('250')) {
|
|
currentStep = 'second_mail_from';
|
|
socket.write('MAIL FROM:<sender2@example.com>\r\n');
|
|
} else if (currentStep === 'second_mail_from' && receivedData.includes('503')) {
|
|
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: DATA without MAIL FROM
|
|
tap.test('Invalid Sequence - should reject DATA without 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 = 'data_without_mail';
|
|
socket.write('DATA\r\n');
|
|
} else if (currentStep === 'data_without_mail' && receivedData.includes('503')) {
|
|
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: Commands after QUIT
|
|
tap.test('Invalid Sequence - should reject commands after QUIT', async (tools) => {
|
|
const done = tools.defer();
|
|
|
|
const socket = net.createConnection({
|
|
host: 'localhost',
|
|
port: TEST_PORT,
|
|
timeout: TEST_TIMEOUT
|
|
});
|
|
|
|
let receivedData = '';
|
|
let currentStep = 'connecting';
|
|
let quitResponseReceived = false;
|
|
|
|
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 = 'quit';
|
|
socket.write('QUIT\r\n');
|
|
} else if (currentStep === 'quit' && receivedData.includes('221')) {
|
|
quitResponseReceived = true;
|
|
// Try to send command after QUIT
|
|
try {
|
|
socket.write('EHLO test.example.com\r\n');
|
|
// If write succeeds, wait to see if we get a response
|
|
setTimeout(() => {
|
|
socket.destroy();
|
|
done.resolve(); // No response expected after QUIT
|
|
}, 1000);
|
|
} catch (err) {
|
|
// Write failed - connection already closed
|
|
done.resolve();
|
|
}
|
|
}
|
|
});
|
|
|
|
socket.on('close', () => {
|
|
if (quitResponseReceived) {
|
|
done.resolve();
|
|
}
|
|
});
|
|
|
|
socket.on('error', (error) => {
|
|
if (quitResponseReceived && error.message.includes('EPIPE')) {
|
|
done.resolve();
|
|
} else {
|
|
done.reject(error);
|
|
}
|
|
});
|
|
|
|
socket.on('timeout', () => {
|
|
socket.destroy();
|
|
done.reject(new Error(`Connection timeout at step: ${currentStep}`));
|
|
});
|
|
|
|
await done.promise;
|
|
});
|
|
|
|
// Test: RCPT TO without proper email brackets
|
|
tap.test('Invalid Sequence - should handle commands with wrong syntax in sequence', 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 = 'bad_rcpt';
|
|
// RCPT TO with wrong syntax
|
|
socket.write('RCPT TO:recipient@example.com\r\n'); // Missing brackets
|
|
} else if (currentStep === 'bad_rcpt' && receivedData.includes('501')) {
|
|
// After syntax error, try valid command
|
|
currentStep = 'valid_rcpt';
|
|
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
|
} else if (currentStep === 'valid_rcpt' && receivedData.includes('250')) {
|
|
socket.write('QUIT\r\n');
|
|
setTimeout(() => {
|
|
socket.destroy();
|
|
expect(receivedData).toInclude('501'); // Syntax error
|
|
expect(receivedData).toInclude('250'); // Valid command worked
|
|
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(); |