dcrouter/test/suite/smtpclient_rfc-compliance/test.crfc-07.interoperability.ts
2025-05-26 14:50:55 +00:00

728 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { expect, tap } from '@git.zone/tstest/tapbundle';
import { createTestServer } from '../../helpers/server.loader.js';
import { createTestSmtpClient } from '../../helpers/smtp.client.js';
import { Email } from '../../../ts/index.js';
tap.test('CRFC-07: should ensure SMTP interoperability (RFC 5321)', async (tools) => {
const testId = 'CRFC-07-interoperability';
console.log(`\n${testId}: Testing SMTP interoperability compliance...`);
let scenarioCount = 0;
// Scenario 1: Different server implementations compatibility
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing different server implementations`);
const serverImplementations = [
{
name: 'Sendmail-style',
greeting: '220 mail.example.com ESMTP Sendmail 8.15.2/8.15.2; Date Time',
ehloResponse: [
'250-mail.example.com Hello client.example.com [192.168.1.100]',
'250-ENHANCEDSTATUSCODES',
'250-PIPELINING',
'250-8BITMIME',
'250-SIZE 36700160',
'250-DSN',
'250-ETRN',
'250-DELIVERBY',
'250 HELP'
],
quirks: { verboseResponses: true, includesTimestamp: true }
},
{
name: 'Postfix-style',
greeting: '220 mail.example.com ESMTP Postfix',
ehloResponse: [
'250-mail.example.com',
'250-PIPELINING',
'250-SIZE 10240000',
'250-VRFY',
'250-ETRN',
'250-STARTTLS',
'250-ENHANCEDSTATUSCODES',
'250-8BITMIME',
'250-DSN',
'250 SMTPUTF8'
],
quirks: { shortResponses: true, strictSyntax: true }
},
{
name: 'Exchange-style',
greeting: '220 mail.example.com Microsoft ESMTP MAIL Service ready',
ehloResponse: [
'250-mail.example.com Hello [192.168.1.100]',
'250-SIZE 37748736',
'250-PIPELINING',
'250-DSN',
'250-ENHANCEDSTATUSCODES',
'250-STARTTLS',
'250-8BITMIME',
'250-BINARYMIME',
'250-CHUNKING',
'250 OK'
],
quirks: { windowsLineEndings: true, detailedErrors: true }
}
];
for (const impl of serverImplementations) {
console.log(`\n Testing with ${impl.name} server...`);
const testServer = await createTestServer({
onConnection: async (socket) => {
console.log(` [${impl.name}] Client connected`);
socket.write(impl.greeting + '\r\n');
socket.on('data', (data) => {
const command = data.toString().trim();
console.log(` [${impl.name}] Received: ${command}`);
if (command.startsWith('EHLO')) {
impl.ehloResponse.forEach(line => {
socket.write(line + '\r\n');
});
} else if (command.startsWith('MAIL FROM:')) {
if (impl.quirks.strictSyntax && !command.includes('<')) {
socket.write('501 5.5.4 Syntax error in MAIL command\r\n');
} else {
const response = impl.quirks.verboseResponses ?
'250 2.1.0 Sender OK' : '250 OK';
socket.write(response + '\r\n');
}
} else if (command.startsWith('RCPT TO:')) {
const response = impl.quirks.verboseResponses ?
'250 2.1.5 Recipient OK' : '250 OK';
socket.write(response + '\r\n');
} else if (command === 'DATA') {
const response = impl.quirks.detailedErrors ?
'354 Start mail input; end with <CRLF>.<CRLF>' :
'354 Enter message, ending with "." on a line by itself';
socket.write(response + '\r\n');
} else if (command === '.') {
const timestamp = impl.quirks.includesTimestamp ?
` at ${new Date().toISOString()}` : '';
socket.write(`250 2.0.0 Message accepted for delivery${timestamp}\r\n`);
} else if (command === 'QUIT') {
const response = impl.quirks.verboseResponses ?
'221 2.0.0 Service closing transmission channel' :
'221 Bye';
socket.write(response + '\r\n');
socket.end();
}
});
}
});
const smtpClient = createTestSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false
});
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: `Interoperability test with ${impl.name}`,
text: `Testing compatibility with ${impl.name} server implementation`
});
const result = await smtpClient.sendMail(email);
console.log(` ${impl.name} compatibility: Success`);
expect(result).toBeDefined();
expect(result.messageId).toBeDefined();
await testServer.server.close();
}
})();
// Scenario 2: Character encoding and internationalization
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing character encoding interoperability`);
const testServer = await createTestServer({
onConnection: async (socket) => {
console.log(' [Server] Client connected');
socket.write('220 international.example.com ESMTP\r\n');
let supportsUTF8 = false;
socket.on('data', (data) => {
const command = data.toString();
console.log(` [Server] Received (${data.length} bytes): ${command.trim()}`);
if (command.startsWith('EHLO')) {
socket.write('250-international.example.com\r\n');
socket.write('250-8BITMIME\r\n');
socket.write('250-SMTPUTF8\r\n');
socket.write('250 OK\r\n');
supportsUTF8 = true;
} else if (command.startsWith('MAIL FROM:')) {
// Check for non-ASCII characters
const hasNonASCII = /[^\x00-\x7F]/.test(command);
const hasUTF8Param = command.includes('SMTPUTF8');
console.log(` [Server] Non-ASCII: ${hasNonASCII}, UTF8 param: ${hasUTF8Param}`);
if (hasNonASCII && !hasUTF8Param) {
socket.write('553 5.6.7 Non-ASCII addresses require SMTPUTF8\r\n');
} else {
socket.write('250 OK\r\n');
}
} else if (command.startsWith('RCPT TO:')) {
socket.write('250 OK\r\n');
} else if (command.trim() === 'DATA') {
socket.write('354 Start mail input\r\n');
} else if (command.trim() === '.') {
socket.write('250 OK: International message accepted\r\n');
} else if (command.trim() === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
}
});
const smtpClient = createTestSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false
});
// Test various international character sets
const internationalTests = [
{
desc: 'Latin characters with accents',
from: 'sénder@éxample.com',
to: 'récipient@éxample.com',
subject: 'Tëst with açcénts',
text: 'Café, naïve, résumé, piñata'
},
{
desc: 'Cyrillic characters',
from: 'отправитель@пример.com',
to: 'получатель@пример.com',
subject: 'Тест с кириллицей',
text: 'Привет мир! Это тест с русскими буквами.'
},
{
desc: 'Chinese characters',
from: 'sender@example.com', // ASCII for compatibility
to: 'recipient@example.com',
subject: '测试中文字符',
text: '你好世界!这是一个中文测试。'
},
{
desc: 'Arabic characters',
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'اختبار النص العربي',
text: 'مرحبا بالعالم! هذا اختبار باللغة العربية.'
},
{
desc: 'Emoji and symbols',
from: 'sender@example.com',
to: 'recipient@example.com',
subject: '🎉 Test with emojis 🌟',
text: 'Hello 👋 World 🌍! Testing emojis: 🚀 📧 ✨'
}
];
for (const test of internationalTests) {
console.log(` Testing: ${test.desc}`);
const email = new Email({
from: test.from,
to: [test.to],
subject: test.subject,
text: test.text
});
try {
const result = await smtpClient.sendMail(email);
console.log(` ${test.desc}: Success`);
expect(result).toBeDefined();
} catch (error) {
console.log(` ${test.desc}: Failed - ${error.message}`);
// Some may fail if server doesn't support international addresses
}
}
await testServer.server.close();
})();
// Scenario 3: Message format compatibility
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing message format compatibility`);
const testServer = await createTestServer({
onConnection: async (socket) => {
console.log(' [Server] Client connected');
socket.write('220 formats.example.com ESMTP\r\n');
let inData = false;
let messageContent = '';
socket.on('data', (data) => {
if (inData) {
messageContent += data.toString();
if (messageContent.includes('\r\n.\r\n')) {
inData = false;
// Analyze message format
const headers = messageContent.substring(0, messageContent.indexOf('\r\n\r\n'));
const body = messageContent.substring(messageContent.indexOf('\r\n\r\n') + 4);
console.log(' [Server] Message analysis:');
console.log(` Header count: ${(headers.match(/\r\n/g) || []).length + 1}`);
console.log(` Body size: ${body.length} bytes`);
// Check for proper header folding
const longHeaders = headers.split('\r\n').filter(h => h.length > 78);
if (longHeaders.length > 0) {
console.log(` Long headers detected: ${longHeaders.length}`);
}
// Check for MIME structure
if (headers.includes('Content-Type:')) {
console.log(' MIME message detected');
}
socket.write('250 OK: Message format validated\r\n');
messageContent = '';
}
return;
}
const command = data.toString().trim();
console.log(` [Server] Received: ${command}`);
if (command.startsWith('EHLO')) {
socket.write('250-formats.example.com\r\n');
socket.write('250-8BITMIME\r\n');
socket.write('250-BINARYMIME\r\n');
socket.write('250 SIZE 52428800\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');
inData = true;
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
}
});
const smtpClient = createTestSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false
});
// Test different message formats
const formatTests = [
{
desc: 'Plain text message',
email: new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Plain text test',
text: 'This is a simple plain text message.'
})
},
{
desc: 'HTML message',
email: new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'HTML test',
html: '<h1>HTML Message</h1><p>This is an <strong>HTML</strong> message.</p>'
})
},
{
desc: 'Multipart alternative',
email: new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Multipart test',
text: 'Plain text version',
html: '<p>HTML version</p>'
})
},
{
desc: 'Message with attachment',
email: new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Attachment test',
text: 'Message with attachment',
attachments: [{
filename: 'test.txt',
content: 'This is a test attachment'
}]
})
},
{
desc: 'Message with custom headers',
email: new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Custom headers test',
text: 'Message with custom headers',
headers: {
'X-Custom-Header': 'Custom value',
'X-Mailer': 'Test Mailer 1.0',
'Message-ID': '<test123@example.com>',
'References': '<ref1@example.com> <ref2@example.com>'
}
})
}
];
for (const test of formatTests) {
console.log(` Testing: ${test.desc}`);
const result = await smtpClient.sendMail(test.email);
console.log(` ${test.desc}: Success`);
expect(result).toBeDefined();
expect(result.messageId).toBeDefined();
}
await testServer.server.close();
})();
// Scenario 4: Error handling interoperability
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing error handling interoperability`);
const testServer = await createTestServer({
onConnection: async (socket) => {
console.log(' [Server] Client connected');
socket.write('220 errors.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-errors.example.com\r\n');
socket.write('250-ENHANCEDSTATUSCODES\r\n');
socket.write('250 OK\r\n');
} else if (command.startsWith('MAIL FROM:')) {
const address = command.match(/<(.+)>/)?.[1] || '';
if (address.includes('temp-fail')) {
// Temporary failure - client should retry
socket.write('451 4.7.1 Temporary system problem, try again later\r\n');
} else if (address.includes('perm-fail')) {
// Permanent failure - client should not retry
socket.write('550 5.1.8 Invalid sender address format\r\n');
} else if (address.includes('syntax-error')) {
// Syntax error
socket.write('501 5.5.4 Syntax error in MAIL command\r\n');
} else {
socket.write('250 OK\r\n');
}
} else if (command.startsWith('RCPT TO:')) {
const address = command.match(/<(.+)>/)?.[1] || '';
if (address.includes('unknown')) {
socket.write('550 5.1.1 User unknown in local recipient table\r\n');
} else if (address.includes('temp-reject')) {
socket.write('450 4.2.1 Mailbox temporarily unavailable\r\n');
} else if (address.includes('quota-exceeded')) {
socket.write('552 5.2.2 Mailbox over quota\r\n');
} else {
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 {
// Unknown command
socket.write('500 5.5.1 Command unrecognized\r\n');
}
});
}
});
const smtpClient = createTestSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false
});
// Test various error scenarios
const errorTests = [
{
desc: 'Temporary sender failure',
from: 'temp-fail@example.com',
to: 'valid@example.com',
expectError: true,
errorType: '4xx'
},
{
desc: 'Permanent sender failure',
from: 'perm-fail@example.com',
to: 'valid@example.com',
expectError: true,
errorType: '5xx'
},
{
desc: 'Unknown recipient',
from: 'valid@example.com',
to: 'unknown@example.com',
expectError: true,
errorType: '5xx'
},
{
desc: 'Mixed valid/invalid recipients',
from: 'valid@example.com',
to: ['valid@example.com', 'unknown@example.com', 'temp-reject@example.com'],
expectError: false, // Partial success
errorType: 'mixed'
}
];
for (const test of errorTests) {
console.log(` Testing: ${test.desc}`);
const email = new Email({
from: test.from,
to: Array.isArray(test.to) ? test.to : [test.to],
subject: `Error test: ${test.desc}`,
text: `Testing error handling for ${test.desc}`
});
try {
const result = await smtpClient.sendMail(email);
if (test.expectError && test.errorType !== 'mixed') {
console.log(` Unexpected success for ${test.desc}`);
} else {
console.log(` ${test.desc}: Handled correctly`);
if (result.rejected && result.rejected.length > 0) {
console.log(` Rejected: ${result.rejected.length} recipients`);
}
if (result.accepted && result.accepted.length > 0) {
console.log(` Accepted: ${result.accepted.length} recipients`);
}
}
} catch (error) {
if (test.expectError) {
console.log(` ${test.desc}: Failed as expected (${error.responseCode})`);
if (test.errorType === '4xx') {
expect(error.responseCode).toBeGreaterThanOrEqual(400);
expect(error.responseCode).toBeLessThan(500);
} else if (test.errorType === '5xx') {
expect(error.responseCode).toBeGreaterThanOrEqual(500);
expect(error.responseCode).toBeLessThan(600);
}
} else {
console.log(` Unexpected error for ${test.desc}: ${error.message}`);
}
}
}
await testServer.server.close();
})();
// Scenario 5: Connection management interoperability
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing connection management interoperability`);
const testServer = await createTestServer({
onConnection: async (socket) => {
console.log(' [Server] Client connected');
let commandCount = 0;
let idleTime = Date.now();
const maxIdleTime = 5000; // 5 seconds for testing
const maxCommands = 10;
socket.write('220 connection.example.com ESMTP\r\n');
// Set up idle timeout
const idleCheck = setInterval(() => {
if (Date.now() - idleTime > maxIdleTime) {
console.log(' [Server] Idle timeout - closing connection');
socket.write('421 4.4.2 Idle timeout, closing connection\r\n');
socket.end();
clearInterval(idleCheck);
}
}, 1000);
socket.on('data', (data) => {
const command = data.toString().trim();
commandCount++;
idleTime = Date.now();
console.log(` [Server] Command ${commandCount}: ${command}`);
if (commandCount > maxCommands) {
console.log(' [Server] Too many commands - closing connection');
socket.write('421 4.7.0 Too many commands, closing connection\r\n');
socket.end();
clearInterval(idleCheck);
return;
}
if (command.startsWith('EHLO')) {
socket.write('250-connection.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\r\n');
} else if (command === 'RSET') {
socket.write('250 OK\r\n');
} else if (command === 'NOOP') {
socket.write('250 OK\r\n');
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
clearInterval(idleCheck);
}
});
socket.on('close', () => {
clearInterval(idleCheck);
console.log(` [Server] Connection closed after ${commandCount} commands`);
});
}
});
const smtpClient = createTestSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 1
});
// Test connection reuse
console.log(' Testing connection reuse...');
for (let i = 1; i <= 3; i++) {
const email = new Email({
from: 'sender@example.com',
to: [`recipient${i}@example.com`],
subject: `Connection test ${i}`,
text: `Testing connection management - email ${i}`
});
const result = await smtpClient.sendMail(email);
console.log(` Email ${i} sent successfully`);
expect(result).toBeDefined();
// Small delay to test connection persistence
await new Promise(resolve => setTimeout(resolve, 500));
}
// Test NOOP for keeping connection alive
console.log(' Testing connection keep-alive...');
await smtpClient.verify(); // This might send NOOP
console.log(' Connection verified (keep-alive)');
await smtpClient.close();
await testServer.server.close();
})();
// Scenario 6: Legacy SMTP compatibility
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing legacy SMTP compatibility`);
const testServer = await createTestServer({
onConnection: async (socket) => {
console.log(' [Server] Legacy SMTP server');
// Old-style greeting without ESMTP
socket.write('220 legacy.example.com Simple Mail Transfer Service Ready\r\n');
socket.on('data', (data) => {
const command = data.toString().trim();
console.log(` [Server] Received: ${command}`);
if (command.startsWith('EHLO')) {
// Legacy server doesn't understand EHLO
socket.write('500 Command unrecognized\r\n');
} else if (command.startsWith('HELO')) {
socket.write('250 legacy.example.com\r\n');
} else if (command.startsWith('MAIL FROM:')) {
// Very strict syntax checking
if (!command.match(/^MAIL FROM:\s*<[^>]+>\s*$/)) {
socket.write('501 Syntax error\r\n');
} else {
socket.write('250 Sender OK\r\n');
}
} else if (command.startsWith('RCPT TO:')) {
if (!command.match(/^RCPT TO:\s*<[^>]+>\s*$/)) {
socket.write('501 Syntax error\r\n');
} else {
socket.write('250 Recipient OK\r\n');
}
} else if (command === 'DATA') {
socket.write('354 Enter mail, end with "." on a line by itself\r\n');
} else if (command === '.') {
socket.write('250 Message accepted for delivery\r\n');
} else if (command === 'QUIT') {
socket.write('221 Service closing transmission channel\r\n');
socket.end();
} else if (command === 'HELP') {
socket.write('214-Commands supported:\r\n');
socket.write('214-HELO MAIL RCPT DATA QUIT HELP\r\n');
socket.write('214 End of HELP info\r\n');
} else {
socket.write('500 Command unrecognized\r\n');
}
});
}
});
// Test with client that can fall back to basic SMTP
const legacyClient = createTestSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
disableESMTP: true // Force HELO mode
});
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Legacy compatibility test',
text: 'Testing compatibility with legacy SMTP servers'
});
const result = await legacyClient.sendMail(email);
console.log(' Legacy SMTP compatibility: Success');
expect(result).toBeDefined();
expect(result.messageId).toBeDefined();
await testServer.server.close();
})();
console.log(`\n${testId}: All ${scenarioCount} interoperability scenarios tested ✓`);
});
tap.start();