dcrouter/test/suite/smtpserver_commands/test.cmd-02.mail-from.ts
2025-05-25 19:05:43 +00:00

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();