dcrouter/test/suite/smtpclient_rfc-compliance/test.crfc-03.command-syntax.ts

522 lines
19 KiB
TypeScript
Raw Normal View History

2025-05-24 18:12:08 +00:00
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as plugins from './plugins.js';
import { createTestServer } from '../../helpers/server.loader.js';
import { createSmtpClient } from '../../helpers/smtp.client.js';
tap.test('CRFC-03: should comply with SMTP command syntax (RFC 5321)', async (tools) => {
const testId = 'CRFC-03-command-syntax';
console.log(`\n${testId}: Testing SMTP command syntax compliance...`);
let scenarioCount = 0;
// Scenario 1: EHLO/HELO command syntax
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing EHLO/HELO command syntax`);
const testServer = await createTestServer({
onConnection: async (socket) => {
console.log(' [Server] Client connected');
socket.write('220 syntax.example.com ESMTP\r\n');
socket.on('data', (data) => {
const command = data.toString().trim();
console.log(` [Server] Received: ${command}`);
if (command.match(/^EHLO\s+[^\s]+$/i)) {
const domain = command.split(' ')[1];
console.log(` [Server] Valid EHLO with domain: ${domain}`);
// Validate domain format (basic check)
if (domain.includes('.') || domain === 'localhost' || domain.match(/^\[[\d\.]+\]$/)) {
socket.write('250-syntax.example.com\r\n');
socket.write('250 OK\r\n');
} else {
socket.write('501 5.5.4 Invalid domain name\r\n');
}
} else if (command.match(/^HELO\s+[^\s]+$/i)) {
const domain = command.split(' ')[1];
console.log(` [Server] Valid HELO with domain: ${domain}`);
socket.write('250 syntax.example.com\r\n');
} else if (command === 'EHLO' || command === 'HELO') {
console.log(' [Server] Missing domain parameter');
socket.write('501 5.5.4 EHLO/HELO requires domain name\r\n');
} else if (command.startsWith('EHLO ') && command.split(' ').length > 2) {
console.log(' [Server] Too many parameters');
socket.write('501 5.5.4 EHLO syntax error\r\n');
} else if (command.startsWith('MAIL FROM:')) {
socket.write('250 OK\r\n');
} else if (command.startsWith('RCPT TO:')) {
socket.write('250 OK\r\n');
} else if (command === 'DATA') {
socket.write('354 Start mail input\r\n');
} else if (command === '.') {
socket.write('250 OK\r\n');
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
} else {
socket.write('500 5.5.1 Command not recognized\r\n');
}
});
}
});
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
name: 'client.example.com' // Valid domain
});
const email = new plugins.smartmail.Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'EHLO syntax test',
text: 'Testing proper EHLO syntax'
});
const result = await smtpClient.sendMail(email);
console.log(' Valid EHLO syntax accepted');
expect(result).toBeDefined();
expect(result.messageId).toBeDefined();
await testServer.server.close();
})();
// Scenario 2: MAIL FROM command syntax
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing MAIL FROM command syntax`);
const testServer = await createTestServer({
onConnection: async (socket) => {
console.log(' [Server] Client connected');
socket.write('220 syntax.example.com ESMTP\r\n');
socket.on('data', (data) => {
const command = data.toString().trim();
console.log(` [Server] Received: ${command}`);
if (command.startsWith('EHLO')) {
socket.write('250-syntax.example.com\r\n');
socket.write('250-SIZE 10485760\r\n');
socket.write('250-8BITMIME\r\n');
socket.write('250 OK\r\n');
} else if (command.match(/^MAIL FROM:\s*<[^>]*>(\s+[A-Z0-9]+=\S*)*\s*$/i)) {
// Valid MAIL FROM syntax with optional parameters
const address = command.match(/<([^>]*)>/)?.[1] || '';
console.log(` [Server] Valid MAIL FROM: ${address}`);
// Validate email address format
if (address === '' || address.includes('@') || address === 'postmaster') {
// Check for ESMTP parameters
const params = command.substring(command.indexOf('>') + 1).trim();
if (params) {
console.log(` [Server] ESMTP parameters: ${params}`);
// Validate parameter syntax
const validParams = /^(\s+[A-Z0-9]+=\S*)*\s*$/i.test(params);
if (validParams) {
socket.write('250 OK\r\n');
} else {
socket.write('501 5.5.4 Invalid MAIL FROM parameters\r\n');
}
} else {
socket.write('250 OK\r\n');
}
} else {
socket.write('553 5.1.8 Invalid sender address\r\n');
}
} else if (command.startsWith('MAIL FROM:')) {
console.log(' [Server] Invalid MAIL FROM syntax');
if (!command.includes('<') || !command.includes('>')) {
socket.write('501 5.5.4 MAIL FROM requires <address>\r\n');
} else {
socket.write('501 5.5.4 Syntax error in MAIL FROM\r\n');
}
} else if (command.startsWith('RCPT TO:')) {
socket.write('250 OK\r\n');
} else if (command === 'DATA') {
socket.write('354 Start mail input\r\n');
} else if (command === '.') {
socket.write('250 OK\r\n');
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
}
});
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false
});
// Test with various sender formats
const testCases = [
{ from: 'sender@example.com', desc: 'normal address' },
{ from: '', desc: 'null sender (bounce)' },
{ from: 'postmaster', desc: 'postmaster without domain' },
{ from: 'user+tag@example.com', desc: 'address with plus extension' }
];
for (const testCase of testCases) {
console.log(` Testing ${testCase.desc}...`);
const email = new plugins.smartmail.Email({
from: testCase.from || 'sender@example.com',
to: ['recipient@example.com'],
subject: `MAIL FROM syntax test: ${testCase.desc}`,
text: `Testing MAIL FROM with ${testCase.desc}`
});
// For null sender, modify the envelope
if (testCase.from === '') {
email.envelope = { from: '', to: ['recipient@example.com'] };
}
const result = await smtpClient.sendMail(email);
expect(result).toBeDefined();
}
await testServer.server.close();
})();
// Scenario 3: RCPT TO command syntax
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing RCPT TO command syntax`);
const testServer = await createTestServer({
onConnection: async (socket) => {
console.log(' [Server] Client connected');
socket.write('220 syntax.example.com ESMTP\r\n');
socket.on('data', (data) => {
const command = data.toString().trim();
console.log(` [Server] Received: ${command}`);
if (command.startsWith('EHLO')) {
socket.write('250-syntax.example.com\r\n');
socket.write('250-DSN\r\n');
socket.write('250 OK\r\n');
} else if (command.startsWith('MAIL FROM:')) {
socket.write('250 OK\r\n');
} else if (command.match(/^RCPT TO:\s*<[^>]*>(\s+[A-Z0-9]+=\S*)*\s*$/i)) {
// Valid RCPT TO syntax with optional parameters
const address = command.match(/<([^>]*)>/)?.[1] || '';
console.log(` [Server] Valid RCPT TO: ${address}`);
// Validate recipient address
if (address.includes('@') && address.split('@').length === 2) {
// Check for DSN parameters
const params = command.substring(command.indexOf('>') + 1).trim();
if (params) {
console.log(` [Server] DSN parameters: ${params}`);
// Validate NOTIFY and ORCPT parameters
if (params.includes('NOTIFY=') || params.includes('ORCPT=')) {
socket.write('250 OK\r\n');
} else {
socket.write('501 5.5.4 Invalid RCPT TO parameters\r\n');
}
} else {
socket.write('250 OK\r\n');
}
} else {
socket.write('553 5.1.3 Invalid recipient address\r\n');
}
} else if (command.startsWith('RCPT TO:')) {
console.log(' [Server] Invalid RCPT TO syntax');
if (!command.includes('<') || !command.includes('>')) {
socket.write('501 5.5.4 RCPT TO requires <address>\r\n');
} else {
socket.write('501 5.5.4 Syntax error in RCPT TO\r\n');
}
} else if (command === 'DATA') {
socket.write('354 Start mail input\r\n');
} else if (command === '.') {
socket.write('250 OK\r\n');
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
}
});
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false
});
// Test with various recipient formats
const recipients = [
'user@example.com',
'user.name@example.com',
'user+tag@example.com',
'user_name@sub.example.com'
];
const email = new plugins.smartmail.Email({
from: 'sender@example.com',
to: recipients,
subject: 'RCPT TO syntax test',
text: 'Testing RCPT TO command syntax'
});
const result = await smtpClient.sendMail(email);
console.log(` Valid RCPT TO syntax for ${recipients.length} recipients`);
expect(result).toBeDefined();
expect(result.accepted?.length).toBe(recipients.length);
await testServer.server.close();
})();
// Scenario 4: DATA command and message termination
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing DATA command and message termination`);
const testServer = await createTestServer({
onConnection: async (socket) => {
console.log(' [Server] Client connected');
socket.write('220 syntax.example.com ESMTP\r\n');
let inDataMode = false;
let messageData = '';
socket.on('data', (data) => {
if (inDataMode) {
messageData += data.toString();
// Check for proper message termination
if (messageData.includes('\r\n.\r\n')) {
inDataMode = false;
console.log(' [Server] Message terminated with CRLF.CRLF');
// Check for transparency (dot stuffing)
const lines = messageData.split('\r\n');
let hasDotStuffing = false;
lines.forEach(line => {
if (line.startsWith('..')) {
hasDotStuffing = true;
console.log(' [Server] Found dot stuffing in line');
}
});
socket.write('250 OK: Message accepted\r\n');
messageData = '';
}
return;
}
const command = data.toString().trim();
console.log(` [Server] Received: ${command}`);
if (command.startsWith('EHLO')) {
socket.write('250-syntax.example.com\r\n');
socket.write('250 OK\r\n');
} else if (command.startsWith('MAIL FROM:')) {
socket.write('250 OK\r\n');
} else if (command.startsWith('RCPT TO:')) {
socket.write('250 OK\r\n');
} else if (command === 'DATA') {
console.log(' [Server] Entering DATA mode');
socket.write('354 Start mail input; end with <CRLF>.<CRLF>\r\n');
inDataMode = true;
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
}
});
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false
});
// Test with message containing dots at line start (transparency test)
const email = new plugins.smartmail.Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'DATA transparency test',
text: 'Line 1\n.This line starts with a dot\n..This line starts with two dots\nLine 4'
});
const result = await smtpClient.sendMail(email);
console.log(' DATA command and transparency handled correctly');
expect(result).toBeDefined();
expect(result.messageId).toBeDefined();
await testServer.server.close();
})();
// Scenario 5: RSET command syntax
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing RSET command syntax`);
const testServer = await createTestServer({
onConnection: async (socket) => {
console.log(' [Server] Client connected');
socket.write('220 syntax.example.com ESMTP\r\n');
let transactionState = 'initial';
socket.on('data', (data) => {
const command = data.toString().trim();
console.log(` [Server] Received: ${command} (state: ${transactionState})`);
if (command.startsWith('EHLO')) {
socket.write('250-syntax.example.com\r\n');
socket.write('250 OK\r\n');
transactionState = 'ready';
} else if (command.startsWith('MAIL FROM:') && transactionState === 'ready') {
socket.write('250 OK\r\n');
transactionState = 'mail';
} else if (command.startsWith('RCPT TO:') && transactionState === 'mail') {
socket.write('250 OK\r\n');
transactionState = 'rcpt';
} else if (command === 'RSET') {
console.log(' [Server] RSET - resetting transaction state');
socket.write('250 OK\r\n');
transactionState = 'ready';
} else if (command.match(/^RSET\s+/)) {
console.log(' [Server] RSET with parameters - syntax error');
socket.write('501 5.5.4 RSET does not accept parameters\r\n');
} else if (command === 'DATA' && transactionState === 'rcpt') {
socket.write('354 Start mail input\r\n');
} else if (command === '.') {
socket.write('250 OK\r\n');
transactionState = 'ready';
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
} else {
socket.write('503 5.5.1 Bad sequence of commands\r\n');
}
});
}
});
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false
});
// Start a transaction then reset it
const email = new plugins.smartmail.Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'RSET test',
text: 'Testing RSET command'
});
const result = await smtpClient.sendMail(email);
console.log(' RSET command syntax validated');
expect(result).toBeDefined();
await testServer.server.close();
})();
// Scenario 6: Command line length limits
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing command line length limits`);
const maxLineLength = 512; // RFC 5321 limit
const testServer = await createTestServer({
onConnection: async (socket) => {
console.log(' [Server] Client connected');
socket.write('220 syntax.example.com ESMTP\r\n');
let lineBuffer = '';
socket.on('data', (data) => {
lineBuffer += data.toString();
const lines = lineBuffer.split('\r\n');
lineBuffer = lines.pop() || ''; // Keep incomplete line
lines.forEach(line => {
if (line.length === 0) return;
console.log(` [Server] Line length: ${line.length} chars`);
if (line.length > maxLineLength) {
console.log(' [Server] Line too long');
socket.write('500 5.5.1 Line too long\r\n');
return;
}
if (line.startsWith('EHLO')) {
socket.write('250-syntax.example.com\r\n');
socket.write('250 OK\r\n');
} else if (line.startsWith('MAIL FROM:')) {
socket.write('250 OK\r\n');
} else if (line.startsWith('RCPT TO:')) {
socket.write('250 OK\r\n');
} else if (line === 'DATA') {
socket.write('354 Start mail input\r\n');
} else if (line === '.') {
socket.write('250 OK\r\n');
} else if (line === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
});
}
});
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false
});
// Test with normal length commands
const email = new plugins.smartmail.Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Line length test',
text: 'Testing command line length limits'
});
const result = await smtpClient.sendMail(email);
console.log(' Normal command lengths accepted');
expect(result).toBeDefined();
// Test with very long recipient address
const longRecipient = 'very-long-username-that-exceeds-normal-limits@' + 'x'.repeat(400) + '.com';
const longEmail = new plugins.smartmail.Email({
from: 'sender@example.com',
to: [longRecipient],
subject: 'Long recipient test',
text: 'Testing very long recipient address'
});
try {
await smtpClient.sendMail(longEmail);
console.log(' Long command handled (possibly folded)');
} catch (error) {
console.log(' Long command rejected as expected');
expect(error.message).toContain('too long');
}
await testServer.server.close();
})();
console.log(`\n${testId}: All ${scenarioCount} command syntax scenarios tested ✓`);
});