330 lines
10 KiB
TypeScript
330 lines
10 KiB
TypeScript
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
import * as net from 'net';
|
|
import { startTestServer, stopTestServer } from '../../helpers/server.loader.js';
|
|
|
|
const TEST_PORT = 2525;
|
|
|
|
let testServer;
|
|
const TEST_TIMEOUT = 10000;
|
|
|
|
tap.test('prepare server', async () => {
|
|
testServer = await startTestServer({ port: TEST_PORT });
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
});
|
|
|
|
tap.test('CMD-02: MAIL FROM - accepts valid sender addresses', async (tools) => {
|
|
const done = tools.defer();
|
|
|
|
const socket = net.createConnection({
|
|
host: 'localhost',
|
|
port: TEST_PORT,
|
|
timeout: TEST_TIMEOUT
|
|
});
|
|
|
|
let receivedData = '';
|
|
let currentStep = 'connecting';
|
|
let testIndex = 0;
|
|
|
|
const validAddresses = [
|
|
'sender@example.com',
|
|
'test.user+tag@example.com',
|
|
'user@[192.168.1.1]', // IP literal
|
|
'user@subdomain.example.com',
|
|
'user@very-long-domain-name-that-is-still-valid.example.com',
|
|
'test_user@example.com' // underscore in local part
|
|
];
|
|
|
|
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 = '';
|
|
console.log(`Testing valid address: ${validAddresses[testIndex]}`);
|
|
socket.write(`MAIL FROM:<${validAddresses[testIndex]}>\r\n`);
|
|
} else if (currentStep === 'mail_from' && receivedData.includes('250')) {
|
|
testIndex++;
|
|
if (testIndex < validAddresses.length) {
|
|
currentStep = 'rset';
|
|
receivedData = '';
|
|
socket.write('RSET\r\n');
|
|
} else {
|
|
socket.write('QUIT\r\n');
|
|
setTimeout(() => {
|
|
socket.destroy();
|
|
done.resolve();
|
|
}, 100);
|
|
}
|
|
} else if (currentStep === 'rset' && receivedData.includes('250')) {
|
|
currentStep = 'mail_from';
|
|
receivedData = '';
|
|
console.log(`Testing valid address: ${validAddresses[testIndex]}`);
|
|
socket.write(`MAIL FROM:<${validAddresses[testIndex]}>\r\n`);
|
|
}
|
|
});
|
|
|
|
socket.on('error', (error) => {
|
|
done.reject(error);
|
|
});
|
|
|
|
socket.on('timeout', () => {
|
|
socket.destroy();
|
|
done.reject(new Error(`Connection timeout at step: ${currentStep}`));
|
|
});
|
|
|
|
await done.promise;
|
|
});
|
|
|
|
tap.test('CMD-02: MAIL FROM - rejects invalid sender addresses', async (tools) => {
|
|
const done = tools.defer();
|
|
|
|
const socket = net.createConnection({
|
|
host: 'localhost',
|
|
port: TEST_PORT,
|
|
timeout: TEST_TIMEOUT
|
|
});
|
|
|
|
let receivedData = '';
|
|
let currentStep = 'connecting';
|
|
let testIndex = 0;
|
|
|
|
const invalidAddresses = [
|
|
'notanemail', // No @ symbol
|
|
'@example.com', // Missing local part
|
|
'user@', // Missing domain
|
|
'user@.com', // Invalid domain
|
|
'user@domain..com', // Double dot
|
|
'user with spaces@example.com', // Unquoted spaces
|
|
'user@<example.com>', // Invalid characters
|
|
'user@@example.com', // Double @
|
|
'user@localhost' // localhost not valid domain
|
|
];
|
|
|
|
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 = '';
|
|
console.log(`Testing invalid address: "${invalidAddresses[testIndex]}"`);
|
|
socket.write(`MAIL FROM:<${invalidAddresses[testIndex]}>\r\n`);
|
|
} else if (currentStep === 'mail_from' && (receivedData.includes('250') || receivedData.includes('5'))) {
|
|
// Server might accept some addresses or reject with 5xx error
|
|
// For this test, we just verify the server responds appropriately
|
|
console.log(` Response: ${receivedData.trim()}`);
|
|
|
|
testIndex++;
|
|
if (testIndex < invalidAddresses.length) {
|
|
currentStep = 'rset';
|
|
receivedData = '';
|
|
socket.write('RSET\r\n');
|
|
} else {
|
|
socket.write('QUIT\r\n');
|
|
setTimeout(() => {
|
|
socket.destroy();
|
|
done.resolve();
|
|
}, 100);
|
|
}
|
|
} else if (currentStep === 'rset' && receivedData.includes('250')) {
|
|
currentStep = 'mail_from';
|
|
receivedData = '';
|
|
console.log(`Testing invalid address: "${invalidAddresses[testIndex]}"`);
|
|
socket.write(`MAIL FROM:<${invalidAddresses[testIndex]}>\r\n`);
|
|
}
|
|
});
|
|
|
|
socket.on('error', (error) => {
|
|
done.reject(error);
|
|
});
|
|
|
|
socket.on('timeout', () => {
|
|
socket.destroy();
|
|
done.reject(new Error(`Connection timeout at step: ${currentStep}`));
|
|
});
|
|
|
|
await done.promise;
|
|
});
|
|
|
|
tap.test('CMD-02: MAIL FROM with SIZE parameter', 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';
|
|
receivedData = '';
|
|
socket.write('EHLO test.example.com\r\n');
|
|
} else if (currentStep === 'ehlo' && receivedData.includes('250 ')) {
|
|
currentStep = 'mail_from_small';
|
|
receivedData = '';
|
|
// Test small size
|
|
socket.write('MAIL FROM:<sender@example.com> SIZE=1024\r\n');
|
|
} else if (currentStep === 'mail_from_small' && receivedData.includes('250')) {
|
|
currentStep = 'rset';
|
|
receivedData = '';
|
|
socket.write('RSET\r\n');
|
|
} else if (currentStep === 'rset' && receivedData.includes('250')) {
|
|
currentStep = 'mail_from_large';
|
|
receivedData = '';
|
|
// Test large size (should be rejected if exceeds limit)
|
|
socket.write('MAIL FROM:<sender@example.com> SIZE=99999999\r\n');
|
|
} else if (currentStep === 'mail_from_large') {
|
|
// Should get either 250 (accepted) or 552 (message size exceeds limit)
|
|
expect(receivedData).toMatch(/^(250|552)/);
|
|
socket.write('QUIT\r\n');
|
|
setTimeout(() => {
|
|
socket.destroy();
|
|
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;
|
|
});
|
|
|
|
tap.test('CMD-02: MAIL FROM with 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';
|
|
receivedData = '';
|
|
socket.write('EHLO test.example.com\r\n');
|
|
} else if (currentStep === 'ehlo' && receivedData.includes('250 ')) {
|
|
currentStep = 'mail_from_8bitmime';
|
|
receivedData = '';
|
|
// Test BODY=8BITMIME
|
|
socket.write('MAIL FROM:<sender@example.com> BODY=8BITMIME\r\n');
|
|
} else if (currentStep === 'mail_from_8bitmime' && receivedData.includes('250')) {
|
|
currentStep = 'rset';
|
|
receivedData = '';
|
|
socket.write('RSET\r\n');
|
|
} else if (currentStep === 'rset' && receivedData.includes('250')) {
|
|
currentStep = 'mail_from_unknown';
|
|
receivedData = '';
|
|
// Test unknown parameter (should be ignored or rejected)
|
|
socket.write('MAIL FROM:<sender@example.com> UNKNOWN=value\r\n');
|
|
} else if (currentStep === 'mail_from_unknown') {
|
|
// Should get either 250 (ignored) or 555 (parameter not recognized)
|
|
expect(receivedData).toMatch(/^(250|555|501)/);
|
|
socket.write('QUIT\r\n');
|
|
setTimeout(() => {
|
|
socket.destroy();
|
|
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;
|
|
});
|
|
|
|
tap.test('CMD-02: MAIL FROM sequence violations', 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_without_ehlo';
|
|
receivedData = '';
|
|
// Try MAIL FROM without EHLO/HELO first
|
|
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
|
} else if (currentStep === 'mail_without_ehlo' && receivedData.includes('503')) {
|
|
// Should get 503 (bad sequence)
|
|
currentStep = 'ehlo';
|
|
receivedData = '';
|
|
socket.write('EHLO test.example.com\r\n');
|
|
} else if (currentStep === 'ehlo' && receivedData.includes('250 ')) {
|
|
currentStep = 'first_mail';
|
|
receivedData = '';
|
|
socket.write('MAIL FROM:<first@example.com>\r\n');
|
|
} else if (currentStep === 'first_mail' && receivedData.includes('250')) {
|
|
currentStep = 'second_mail';
|
|
receivedData = '';
|
|
// Try second MAIL FROM without RSET
|
|
socket.write('MAIL FROM:<second@example.com>\r\n');
|
|
} else if (currentStep === 'second_mail' && (receivedData.includes('503') || receivedData.includes('250'))) {
|
|
// Server might accept or reject the second MAIL FROM
|
|
// Some servers allow resetting the sender, others require RSET
|
|
console.log(`Second MAIL FROM response: ${receivedData.trim()}`);
|
|
socket.write('QUIT\r\n');
|
|
setTimeout(() => {
|
|
socket.destroy();
|
|
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;
|
|
});
|
|
|
|
tap.test('cleanup server', async () => {
|
|
await stopTestServer(testServer);
|
|
});
|
|
|
|
export default tap.start(); |