dcrouter/test/suite/smtpclient_rfc-compliance/test.crfc-02.esmtp-compliance.ts
2025-05-25 19:02:18 +00:00

548 lines
19 KiB
TypeScript

import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as plugins from '../../../ts/plugins.js';
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
tap.test('CRFC-02: should comply with ESMTP extensions (RFC 1869)', async (tools) => {
const testId = 'CRFC-02-esmtp-compliance';
console.log(`\n${testId}: Testing ESMTP extension compliance...`);
let scenarioCount = 0;
// Scenario 1: EHLO vs HELO negotiation
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing EHLO vs HELO negotiation`);
const testServer = await createTestServer({
onConnection: async (socket) => {
console.log(' [Server] Client connected');
socket.write('220 esmtp.example.com ESMTP Service Ready\r\n');
socket.on('data', (data) => {
const command = data.toString().trim();
console.log(` [Server] Received: ${command}`);
if (command.startsWith('EHLO')) {
// ESMTP response with extensions
socket.write('250-esmtp.example.com Hello\r\n');
socket.write('250-SIZE 35882577\r\n');
socket.write('250-8BITMIME\r\n');
socket.write('250-STARTTLS\r\n');
socket.write('250-ENHANCEDSTATUSCODES\r\n');
socket.write('250-PIPELINING\r\n');
socket.write('250-CHUNKING\r\n');
socket.write('250-SMTPUTF8\r\n');
socket.write('250 DSN\r\n');
} else if (command.startsWith('HELO')) {
// Basic SMTP response
socket.write('250 esmtp.example.com\r\n');
} else if (command.startsWith('MAIL FROM:')) {
// Check for ESMTP parameters
if (command.includes('SIZE=')) {
console.log(' [Server] SIZE parameter detected');
}
if (command.includes('BODY=')) {
console.log(' [Server] BODY parameter detected');
}
socket.write('250 2.1.0 OK\r\n');
} else if (command.startsWith('RCPT TO:')) {
// Check for DSN parameters
if (command.includes('NOTIFY=')) {
console.log(' [Server] NOTIFY parameter detected');
}
if (command.includes('ORCPT=')) {
console.log(' [Server] ORCPT parameter detected');
}
socket.write('250 2.1.5 OK\r\n');
} else if (command === 'DATA') {
socket.write('354 Start mail input\r\n');
} else if (command === '.') {
socket.write('250 2.0.0 OK: Message accepted\r\n');
} else if (command === 'QUIT') {
socket.write('221 2.0.0 Bye\r\n');
socket.end();
}
});
}
});
// Test EHLO with ESMTP client
const esmtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
name: 'client.example.com'
});
const email = new plugins.smartmail.Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'ESMTP test',
text: 'Testing ESMTP negotiation'
});
const result = await esmtpClient.sendMail(email);
console.log(' EHLO negotiation successful');
expect(result).toBeDefined();
expect(result.messageId).toBeDefined();
// Test fallback to HELO
const basicClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
name: 'basic.example.com',
disableESMTP: true // Force HELO
});
const heloResult = await basicClient.sendMail(email);
console.log(' HELO fallback successful');
expect(heloResult).toBeDefined();
await testServer.server.close();
})();
// Scenario 2: SIZE extension compliance
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing SIZE extension compliance`);
const maxSize = 5242880; // 5MB
const testServer = await createTestServer({
onConnection: async (socket) => {
console.log(' [Server] Client connected');
socket.write('220 size.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-size.example.com\r\n');
socket.write(`250-SIZE ${maxSize}\r\n`);
socket.write('250 OK\r\n');
} else if (command.startsWith('MAIL FROM:')) {
// Extract SIZE parameter
const sizeMatch = command.match(/SIZE=(\d+)/i);
if (sizeMatch) {
const declaredSize = parseInt(sizeMatch[1]);
console.log(` [Server] Client declared size: ${declaredSize}`);
if (declaredSize > maxSize) {
socket.write(`552 5.3.4 Message size exceeds fixed maximum message size (${maxSize})\r\n`);
} else {
socket.write('250 OK\r\n');
}
} else {
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();
}
});
}
});
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false
});
// Test 1: Small message
const smallEmail = new plugins.smartmail.Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Small message',
text: 'This is a small message'
});
const smallResult = await smtpClient.sendMail(smallEmail);
console.log(' Small message accepted');
expect(smallResult).toBeDefined();
// Test 2: Large message
const largeEmail = new plugins.smartmail.Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Large message',
text: 'X'.repeat(maxSize + 1000) // Exceed limit
});
try {
await smtpClient.sendMail(largeEmail);
console.log(' Unexpected: Large message accepted');
} catch (error) {
console.log(' Large message rejected as expected');
expect(error.message).toContain('size exceeds');
}
await testServer.server.close();
})();
// Scenario 3: 8BITMIME extension
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing 8BITMIME extension`);
const testServer = await createTestServer({
onConnection: async (socket) => {
console.log(' [Server] Client connected');
socket.write('220 8bit.example.com ESMTP\r\n');
let bodyType = '7BIT';
socket.on('data', (data) => {
const command = data.toString().trim();
console.log(` [Server] Received: ${command}`);
if (command.startsWith('EHLO')) {
socket.write('250-8bit.example.com\r\n');
socket.write('250-8BITMIME\r\n');
socket.write('250 OK\r\n');
} else if (command.startsWith('MAIL FROM:')) {
// Check BODY parameter
const bodyMatch = command.match(/BODY=(\w+)/i);
if (bodyMatch) {
bodyType = bodyMatch[1].toUpperCase();
console.log(` [Server] BODY type: ${bodyType}`);
if (bodyType === '8BITMIME' || bodyType === '7BIT') {
socket.write('250 OK\r\n');
} else {
socket.write('555 5.5.4 Unsupported BODY type\r\n');
}
} else {
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: Message accepted (BODY=${bodyType})\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 8-bit content
const email8bit = new plugins.smartmail.Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Testing 8BITMIME with UTF-8: café, naïve, 你好',
text: 'Message with 8-bit characters: émojis 🎉, spéçiål çhåracters, 日本語',
encoding: '8bit'
});
const result = await smtpClient.sendMail(email8bit);
console.log(' 8BITMIME message accepted');
expect(result).toBeDefined();
expect(result.response).toContain('BODY=8BITMIME');
await testServer.server.close();
})();
// Scenario 4: PIPELINING extension
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing PIPELINING extension`);
const testServer = await createTestServer({
onConnection: async (socket) => {
console.log(' [Server] Client connected');
socket.write('220 pipeline.example.com ESMTP\r\n');
let commandBuffer: string[] = [];
socket.on('data', (data) => {
const commands = data.toString().split('\r\n').filter(cmd => cmd.length > 0);
// With pipelining, multiple commands can arrive at once
if (commands.length > 1) {
console.log(` [Server] Received ${commands.length} pipelined commands`);
}
commands.forEach(command => {
console.log(` [Server] Processing: ${command}`);
commandBuffer.push(command);
});
// Process buffered commands
while (commandBuffer.length > 0) {
const command = commandBuffer.shift()!;
if (command.startsWith('EHLO')) {
socket.write('250-pipeline.example.com\r\n');
socket.write('250-PIPELINING\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') {
socket.write('354 Start mail input\r\n');
} else if (command === '.') {
socket.write('250 OK: Pipelined message accepted\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,
pipelining: true
});
// Send email with multiple recipients (tests pipelining)
const email = new plugins.smartmail.Email({
from: 'sender@example.com',
to: ['recipient1@example.com', 'recipient2@example.com', 'recipient3@example.com'],
subject: 'Pipelining test',
text: 'Testing SMTP command pipelining'
});
const result = await smtpClient.sendMail(email);
console.log(' Pipelined commands successful');
expect(result).toBeDefined();
expect(result.response).toContain('Pipelined');
await testServer.server.close();
})();
// Scenario 5: DSN (Delivery Status Notification) extension
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing DSN extension`);
const testServer = await createTestServer({
onConnection: async (socket) => {
console.log(' [Server] Client connected');
socket.write('220 dsn.example.com ESMTP\r\n');
let envid = '';
const recipients: Array<{ address: string; notify: string; orcpt: string }> = [];
socket.on('data', (data) => {
const command = data.toString().trim();
console.log(` [Server] Received: ${command}`);
if (command.startsWith('EHLO')) {
socket.write('250-dsn.example.com\r\n');
socket.write('250-DSN\r\n');
socket.write('250 OK\r\n');
} else if (command.startsWith('MAIL FROM:')) {
// Check for ENVID parameter
const envidMatch = command.match(/ENVID=([^\s]+)/i);
if (envidMatch) {
envid = envidMatch[1];
console.log(` [Server] ENVID: ${envid}`);
}
// Check for RET parameter
const retMatch = command.match(/RET=(FULL|HDRS)/i);
if (retMatch) {
console.log(` [Server] RET: ${retMatch[1]}`);
}
socket.write('250 OK\r\n');
} else if (command.startsWith('RCPT TO:')) {
const address = command.match(/<(.+)>/)?.[1] || '';
let notify = 'NEVER';
let orcpt = '';
// Check NOTIFY parameter
const notifyMatch = command.match(/NOTIFY=([^\s]+)/i);
if (notifyMatch) {
notify = notifyMatch[1];
console.log(` [Server] NOTIFY for ${address}: ${notify}`);
}
// Check ORCPT parameter
const orcptMatch = command.match(/ORCPT=([^\s]+)/i);
if (orcptMatch) {
orcpt = orcptMatch[1];
console.log(` [Server] ORCPT: ${orcpt}`);
}
recipients.push({ address, notify, orcpt });
socket.write('250 OK\r\n');
} else if (command === 'DATA') {
socket.write('354 Start mail input\r\n');
} else if (command === '.') {
const dsnInfo = envid ? ` ENVID=${envid}` : '';
socket.write(`250 OK: Message accepted with DSN${dsnInfo}\r\n`);
// Reset for next message
envid = '';
recipients.length = 0;
} 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 DSN options
const email = new plugins.smartmail.Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'DSN test',
text: 'Testing Delivery Status Notifications',
dsn: {
notify: ['SUCCESS', 'FAILURE', 'DELAY'],
envid: 'unique-message-id-12345',
ret: 'FULL'
}
});
const result = await smtpClient.sendMail(email);
console.log(' DSN parameters accepted');
expect(result).toBeDefined();
expect(result.response).toContain('DSN');
await testServer.server.close();
})();
// Scenario 6: ENHANCEDSTATUSCODES extension
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing ENHANCEDSTATUSCODES extension`);
const testServer = await createTestServer({
onConnection: async (socket) => {
console.log(' [Server] Client connected');
socket.write('220 enhanced.example.com ESMTP\r\n');
let useEnhancedCodes = false;
socket.on('data', (data) => {
const command = data.toString().trim();
console.log(` [Server] Received: ${command}`);
if (command.startsWith('EHLO')) {
socket.write('250-enhanced.example.com\r\n');
socket.write('250-ENHANCEDSTATUSCODES\r\n');
socket.write('250 2.0.0 OK\r\n');
useEnhancedCodes = true;
} else if (command.startsWith('HELO')) {
socket.write('250 enhanced.example.com\r\n');
useEnhancedCodes = false;
} else if (command.startsWith('MAIL FROM:')) {
if (useEnhancedCodes) {
socket.write('250 2.1.0 Sender OK\r\n');
} else {
socket.write('250 Sender OK\r\n');
}
} else if (command.startsWith('RCPT TO:')) {
const address = command.match(/<(.+)>/)?.[1] || '';
if (address.includes('unknown')) {
if (useEnhancedCodes) {
socket.write('550 5.1.1 User unknown\r\n');
} else {
socket.write('550 User unknown\r\n');
}
} else {
if (useEnhancedCodes) {
socket.write('250 2.1.5 Recipient OK\r\n');
} else {
socket.write('250 Recipient OK\r\n');
}
}
} else if (command === 'DATA') {
if (useEnhancedCodes) {
socket.write('354 2.0.0 Start mail input\r\n');
} else {
socket.write('354 Start mail input\r\n');
}
} else if (command === '.') {
if (useEnhancedCodes) {
socket.write('250 2.0.0 Message accepted for delivery\r\n');
} else {
socket.write('250 Message accepted for delivery\r\n');
}
} else if (command === 'QUIT') {
if (useEnhancedCodes) {
socket.write('221 2.0.0 Service closing transmission channel\r\n');
} else {
socket.write('221 Service closing transmission channel\r\n');
}
socket.end();
}
});
}
});
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false
});
// Test with valid recipient
const validEmail = new plugins.smartmail.Email({
from: 'sender@example.com',
to: ['valid@example.com'],
subject: 'Enhanced status codes test',
text: 'Testing enhanced SMTP status codes'
});
const result = await smtpClient.sendMail(validEmail);
console.log(' Enhanced status codes received');
expect(result).toBeDefined();
expect(result.response).toMatch(/2\.\d\.\d/); // Enhanced code format
// Test with invalid recipient
const invalidEmail = new plugins.smartmail.Email({
from: 'sender@example.com',
to: ['unknown@example.com'],
subject: 'Invalid recipient test',
text: 'Testing enhanced error codes'
});
try {
await smtpClient.sendMail(invalidEmail);
console.log(' Unexpected: Invalid recipient accepted');
} catch (error) {
console.log(' Enhanced error code received');
expect(error.responseCode).toBe(550);
expect(error.response).toContain('5.1.1');
}
await testServer.server.close();
})();
console.log(`\n${testId}: All ${scenarioCount} ESMTP compliance scenarios tested ✓`);
});