511 lines
20 KiB
TypeScript
511 lines
20 KiB
TypeScript
|
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-04: should handle SMTP response codes correctly (RFC 5321)', async (tools) => {
|
||
|
const testId = 'CRFC-04-response-codes';
|
||
|
console.log(`\n${testId}: Testing SMTP response code compliance...`);
|
||
|
|
||
|
let scenarioCount = 0;
|
||
|
|
||
|
// Scenario 1: 2xx success response codes
|
||
|
await (async () => {
|
||
|
scenarioCount++;
|
||
|
console.log(`\nScenario ${scenarioCount}: Testing 2xx success response codes`);
|
||
|
|
||
|
const testServer = await createTestServer({
|
||
|
onConnection: async (socket) => {
|
||
|
console.log(' [Server] Client connected');
|
||
|
socket.write('220 responses.example.com Service ready\r\n');
|
||
|
|
||
|
socket.on('data', (data) => {
|
||
|
const command = data.toString().trim();
|
||
|
console.log(` [Server] Received: ${command}`);
|
||
|
|
||
|
if (command.startsWith('EHLO')) {
|
||
|
// 250 - Requested mail action okay, completed
|
||
|
socket.write('250-responses.example.com\r\n');
|
||
|
socket.write('250-SIZE 10485760\r\n');
|
||
|
socket.write('250 OK\r\n');
|
||
|
} else if (command.startsWith('MAIL FROM:')) {
|
||
|
// 250 - Requested mail action okay, completed
|
||
|
socket.write('250 2.1.0 Sender OK\r\n');
|
||
|
} else if (command.startsWith('RCPT TO:')) {
|
||
|
// 250 - Requested mail action okay, completed
|
||
|
socket.write('250 2.1.5 Recipient OK\r\n');
|
||
|
} else if (command === 'DATA') {
|
||
|
// 354 - Start mail input; end with <CRLF>.<CRLF>
|
||
|
socket.write('354 Start mail input; end with <CRLF>.<CRLF>\r\n');
|
||
|
} else if (command === '.') {
|
||
|
// 250 - Requested mail action okay, completed
|
||
|
socket.write('250 2.0.0 Message accepted for delivery\r\n');
|
||
|
} else if (command === 'QUIT') {
|
||
|
// 221 - Service closing transmission channel
|
||
|
socket.write('221 2.0.0 Service closing transmission channel\r\n');
|
||
|
socket.end();
|
||
|
} else if (command === 'NOOP') {
|
||
|
// 250 - Requested mail action okay, completed
|
||
|
socket.write('250 2.0.0 OK\r\n');
|
||
|
} else if (command === 'RSET') {
|
||
|
// 250 - Requested mail action okay, completed
|
||
|
socket.write('250 2.0.0 Reset OK\r\n');
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
|
||
|
const smtpClient = createSmtpClient({
|
||
|
host: testServer.hostname,
|
||
|
port: testServer.port,
|
||
|
secure: false
|
||
|
});
|
||
|
|
||
|
const email = new plugins.smartmail.Email({
|
||
|
from: 'sender@example.com',
|
||
|
to: ['recipient@example.com'],
|
||
|
subject: '2xx response test',
|
||
|
text: 'Testing 2xx success response codes'
|
||
|
});
|
||
|
|
||
|
const result = await smtpClient.sendMail(email);
|
||
|
console.log(' All 2xx success codes handled correctly');
|
||
|
expect(result).toBeDefined();
|
||
|
expect(result.messageId).toBeDefined();
|
||
|
|
||
|
await testServer.server.close();
|
||
|
})();
|
||
|
|
||
|
// Scenario 2: 4xx temporary failure response codes
|
||
|
await (async () => {
|
||
|
scenarioCount++;
|
||
|
console.log(`\nScenario ${scenarioCount}: Testing 4xx temporary failure response codes`);
|
||
|
|
||
|
let attemptCount = 0;
|
||
|
|
||
|
const testServer = await createTestServer({
|
||
|
onConnection: async (socket) => {
|
||
|
attemptCount++;
|
||
|
console.log(` [Server] Client connected (attempt ${attemptCount})`);
|
||
|
socket.write('220 responses.example.com Service ready\r\n');
|
||
|
|
||
|
socket.on('data', (data) => {
|
||
|
const command = data.toString().trim();
|
||
|
console.log(` [Server] Received: ${command}`);
|
||
|
|
||
|
if (command.startsWith('EHLO')) {
|
||
|
socket.write('250-responses.example.com\r\n');
|
||
|
socket.write('250 OK\r\n');
|
||
|
} else if (command.startsWith('MAIL FROM:')) {
|
||
|
if (attemptCount === 1) {
|
||
|
// 451 - Requested action aborted: local error in processing
|
||
|
socket.write('451 4.3.0 Temporary system failure, try again later\r\n');
|
||
|
} else {
|
||
|
socket.write('250 OK\r\n');
|
||
|
}
|
||
|
} else if (command.startsWith('RCPT TO:')) {
|
||
|
const address = command.match(/<(.+)>/)?.[1] || '';
|
||
|
|
||
|
if (address.includes('full')) {
|
||
|
// 452 - Requested action not taken: insufficient system storage
|
||
|
socket.write('452 4.2.2 Mailbox full, try again later\r\n');
|
||
|
} else if (address.includes('busy')) {
|
||
|
// 450 - Requested mail action not taken: mailbox unavailable
|
||
|
socket.write('450 4.2.1 Mailbox busy, try again later\r\n');
|
||
|
} else {
|
||
|
socket.write('250 OK\r\n');
|
||
|
}
|
||
|
} else if (command === 'DATA') {
|
||
|
socket.write('354 Start mail input\r\n');
|
||
|
} else if (command === '.') {
|
||
|
if (attemptCount === 1) {
|
||
|
// 421 - Service not available, closing transmission channel
|
||
|
socket.write('421 4.3.2 System shutting down, try again later\r\n');
|
||
|
socket.end();
|
||
|
} else {
|
||
|
socket.write('250 OK\r\n');
|
||
|
}
|
||
|
} else if (command === 'QUIT') {
|
||
|
socket.write('221 Bye\r\n');
|
||
|
socket.end();
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// Test temporary failures with retry
|
||
|
const smtpClient = createSmtpClient({
|
||
|
host: testServer.hostname,
|
||
|
port: testServer.port,
|
||
|
secure: false
|
||
|
});
|
||
|
|
||
|
// First attempt with temporary failure
|
||
|
const email = new plugins.smartmail.Email({
|
||
|
from: 'sender@example.com',
|
||
|
to: ['recipient@example.com'],
|
||
|
subject: '4xx response test',
|
||
|
text: 'Testing 4xx temporary failure codes'
|
||
|
});
|
||
|
|
||
|
try {
|
||
|
await smtpClient.sendMail(email);
|
||
|
console.log(' Unexpected: First attempt succeeded');
|
||
|
} catch (error) {
|
||
|
console.log(' Expected: Temporary failure on first attempt');
|
||
|
expect(error.responseCode).toBeGreaterThanOrEqual(400);
|
||
|
expect(error.responseCode).toBeLessThan(500);
|
||
|
}
|
||
|
|
||
|
// Second attempt should succeed
|
||
|
const retryResult = await smtpClient.sendMail(email);
|
||
|
console.log(' Retry after temporary failure succeeded');
|
||
|
expect(retryResult).toBeDefined();
|
||
|
|
||
|
// Test specific 4xx codes
|
||
|
const tempFailureEmail = new plugins.smartmail.Email({
|
||
|
from: 'sender@example.com',
|
||
|
to: ['full@example.com', 'busy@example.com'],
|
||
|
subject: 'Specific 4xx test',
|
||
|
text: 'Testing specific temporary failure codes'
|
||
|
});
|
||
|
|
||
|
try {
|
||
|
const result = await smtpClient.sendMail(tempFailureEmail);
|
||
|
console.log(` Partial delivery: ${result.rejected?.length || 0} rejected`);
|
||
|
expect(result.rejected?.length).toBeGreaterThan(0);
|
||
|
} catch (error) {
|
||
|
console.log(' Multiple 4xx failures handled');
|
||
|
}
|
||
|
|
||
|
await testServer.server.close();
|
||
|
})();
|
||
|
|
||
|
// Scenario 3: 5xx permanent failure response codes
|
||
|
await (async () => {
|
||
|
scenarioCount++;
|
||
|
console.log(`\nScenario ${scenarioCount}: Testing 5xx permanent failure response codes`);
|
||
|
|
||
|
const testServer = await createTestServer({
|
||
|
onConnection: async (socket) => {
|
||
|
console.log(' [Server] Client connected');
|
||
|
socket.write('220 responses.example.com Service ready\r\n');
|
||
|
|
||
|
socket.on('data', (data) => {
|
||
|
const command = data.toString().trim();
|
||
|
console.log(` [Server] Received: ${command}`);
|
||
|
|
||
|
if (command.startsWith('EHLO')) {
|
||
|
socket.write('250-responses.example.com\r\n');
|
||
|
socket.write('250 OK\r\n');
|
||
|
} else if (command.startsWith('MAIL FROM:')) {
|
||
|
const address = command.match(/<(.+)>/)?.[1] || '';
|
||
|
|
||
|
if (address.includes('blocked')) {
|
||
|
// 550 - Requested action not taken: mailbox unavailable
|
||
|
socket.write('550 5.1.1 Sender blocked\r\n');
|
||
|
} else if (address.includes('invalid')) {
|
||
|
// 553 - Requested action not taken: mailbox name not allowed
|
||
|
socket.write('553 5.1.8 Invalid sender address format\r\n');
|
||
|
} else {
|
||
|
socket.write('250 OK\r\n');
|
||
|
}
|
||
|
} else if (command.startsWith('RCPT TO:')) {
|
||
|
const address = command.match(/<(.+)>/)?.[1] || '';
|
||
|
|
||
|
if (address.includes('unknown')) {
|
||
|
// 550 - Requested action not taken: mailbox unavailable
|
||
|
socket.write('550 5.1.1 User unknown\r\n');
|
||
|
} else if (address.includes('disabled')) {
|
||
|
// 551 - User not local; please try <forward-path>
|
||
|
socket.write('551 5.1.6 User account disabled\r\n');
|
||
|
} else if (address.includes('relay')) {
|
||
|
// 554 - Transaction failed
|
||
|
socket.write('554 5.7.1 Relay access denied\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 if (command.startsWith('INVALID')) {
|
||
|
// 500 - Syntax error, command unrecognized
|
||
|
socket.write('500 5.5.1 Command not recognized\r\n');
|
||
|
} else if (command === 'MAIL') {
|
||
|
// 501 - Syntax error in parameters or arguments
|
||
|
socket.write('501 5.5.4 Syntax error in MAIL command\r\n');
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
|
||
|
const smtpClient = createSmtpClient({
|
||
|
host: testServer.hostname,
|
||
|
port: testServer.port,
|
||
|
secure: false
|
||
|
});
|
||
|
|
||
|
// Test various 5xx permanent failures
|
||
|
const testCases = [
|
||
|
{ from: 'blocked@example.com', to: 'recipient@example.com', desc: 'blocked sender' },
|
||
|
{ from: 'sender@example.com', to: 'unknown@example.com', desc: 'unknown recipient' },
|
||
|
{ from: 'sender@example.com', to: 'disabled@example.com', desc: 'disabled user' },
|
||
|
{ from: 'sender@example.com', to: 'relay@external.com', desc: 'relay denied' }
|
||
|
];
|
||
|
|
||
|
for (const testCase of testCases) {
|
||
|
console.log(` Testing ${testCase.desc}...`);
|
||
|
|
||
|
const email = new plugins.smartmail.Email({
|
||
|
from: testCase.from,
|
||
|
to: [testCase.to],
|
||
|
subject: `5xx test: ${testCase.desc}`,
|
||
|
text: `Testing 5xx permanent failure: ${testCase.desc}`
|
||
|
});
|
||
|
|
||
|
try {
|
||
|
await smtpClient.sendMail(email);
|
||
|
console.log(` Unexpected: ${testCase.desc} succeeded`);
|
||
|
} catch (error) {
|
||
|
console.log(` Expected: ${testCase.desc} failed with 5xx`);
|
||
|
expect(error.responseCode).toBeGreaterThanOrEqual(500);
|
||
|
expect(error.responseCode).toBeLessThan(600);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
await testServer.server.close();
|
||
|
})();
|
||
|
|
||
|
// Scenario 4: Multi-line response handling
|
||
|
await (async () => {
|
||
|
scenarioCount++;
|
||
|
console.log(`\nScenario ${scenarioCount}: Testing multi-line response handling`);
|
||
|
|
||
|
const testServer = await createTestServer({
|
||
|
onConnection: async (socket) => {
|
||
|
console.log(' [Server] Client connected');
|
||
|
socket.write('220-responses.example.com ESMTP Service Ready\r\n');
|
||
|
socket.write('220-This server supports multiple extensions\r\n');
|
||
|
socket.write('220 Please proceed with EHLO\r\n');
|
||
|
|
||
|
socket.on('data', (data) => {
|
||
|
const command = data.toString().trim();
|
||
|
console.log(` [Server] Received: ${command}`);
|
||
|
|
||
|
if (command.startsWith('EHLO')) {
|
||
|
// Multi-line EHLO response
|
||
|
socket.write('250-responses.example.com Hello client\r\n');
|
||
|
socket.write('250-SIZE 10485760\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-DSN\r\n');
|
||
|
socket.write('250 HELP\r\n'); // Last line ends with space
|
||
|
} 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; end with <CRLF>.<CRLF>\r\n');
|
||
|
} else if (command === '.') {
|
||
|
// Multi-line success response
|
||
|
socket.write('250-Message accepted for delivery\r\n');
|
||
|
socket.write('250-Queue ID: ABC123\r\n');
|
||
|
socket.write('250 Thank you\r\n');
|
||
|
} else if (command === 'HELP') {
|
||
|
// Multi-line help response
|
||
|
socket.write('214-This server supports the following commands:\r\n');
|
||
|
socket.write('214-EHLO HELO MAIL RCPT DATA\r\n');
|
||
|
socket.write('214-RSET NOOP QUIT HELP\r\n');
|
||
|
socket.write('214 For more info visit http://example.com/help\r\n');
|
||
|
} else if (command === 'QUIT') {
|
||
|
socket.write('221-Thank you for using our service\r\n');
|
||
|
socket.write('221 Goodbye\r\n');
|
||
|
socket.end();
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
|
||
|
const smtpClient = createSmtpClient({
|
||
|
host: testServer.hostname,
|
||
|
port: testServer.port,
|
||
|
secure: false
|
||
|
});
|
||
|
|
||
|
const email = new plugins.smartmail.Email({
|
||
|
from: 'sender@example.com',
|
||
|
to: ['recipient@example.com'],
|
||
|
subject: 'Multi-line response test',
|
||
|
text: 'Testing multi-line SMTP response handling'
|
||
|
});
|
||
|
|
||
|
const result = await smtpClient.sendMail(email);
|
||
|
console.log(' Multi-line responses handled correctly');
|
||
|
expect(result).toBeDefined();
|
||
|
expect(result.response).toContain('Queue ID');
|
||
|
|
||
|
await testServer.server.close();
|
||
|
})();
|
||
|
|
||
|
// Scenario 5: Response code format validation
|
||
|
await (async () => {
|
||
|
scenarioCount++;
|
||
|
console.log(`\nScenario ${scenarioCount}: Testing response code format validation`);
|
||
|
|
||
|
const testServer = await createTestServer({
|
||
|
onConnection: async (socket) => {
|
||
|
console.log(' [Server] Client connected');
|
||
|
socket.write('220 responses.example.com ESMTP\r\n');
|
||
|
|
||
|
let commandCount = 0;
|
||
|
|
||
|
socket.on('data', (data) => {
|
||
|
commandCount++;
|
||
|
const command = data.toString().trim();
|
||
|
console.log(` [Server] Received: ${command}`);
|
||
|
|
||
|
if (command.startsWith('EHLO')) {
|
||
|
socket.write('250-responses.example.com\r\n');
|
||
|
socket.write('250 OK\r\n');
|
||
|
} else if (command.startsWith('MAIL FROM:')) {
|
||
|
// Test various response code formats
|
||
|
if (commandCount === 2) {
|
||
|
// Valid 3-digit code
|
||
|
socket.write('250 OK\r\n');
|
||
|
} else {
|
||
|
socket.write('250 OK\r\n');
|
||
|
}
|
||
|
} else if (command.startsWith('RCPT TO:')) {
|
||
|
const address = command.match(/<(.+)>/)?.[1] || '';
|
||
|
|
||
|
if (address.includes('enhanced')) {
|
||
|
// Enhanced status code format (RFC 3463)
|
||
|
socket.write('250 2.1.5 Recipient OK\r\n');
|
||
|
} else if (address.includes('detailed')) {
|
||
|
// Detailed response with explanation
|
||
|
socket.write('250 OK: Recipient accepted for delivery to local mailbox\r\n');
|
||
|
} else {
|
||
|
socket.write('250 OK\r\n');
|
||
|
}
|
||
|
} else if (command === 'DATA') {
|
||
|
socket.write('354 Start mail input; end with <CRLF>.<CRLF>\r\n');
|
||
|
} else if (command === '.') {
|
||
|
// Response with timestamp
|
||
|
const timestamp = new Date().toISOString();
|
||
|
socket.write(`250 OK: Message accepted at ${timestamp}\r\n`);
|
||
|
} else if (command === 'QUIT') {
|
||
|
socket.write('221 Service closing transmission channel\r\n');
|
||
|
socket.end();
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
|
||
|
const smtpClient = createSmtpClient({
|
||
|
host: testServer.hostname,
|
||
|
port: testServer.port,
|
||
|
secure: false
|
||
|
});
|
||
|
|
||
|
// Test with recipients that trigger different response formats
|
||
|
const email = new plugins.smartmail.Email({
|
||
|
from: 'sender@example.com',
|
||
|
to: ['enhanced@example.com', 'detailed@example.com', 'normal@example.com'],
|
||
|
subject: 'Response format test',
|
||
|
text: 'Testing SMTP response code format compliance'
|
||
|
});
|
||
|
|
||
|
const result = await smtpClient.sendMail(email);
|
||
|
console.log(' Various response code formats handled');
|
||
|
expect(result).toBeDefined();
|
||
|
expect(result.response).toContain('Message accepted');
|
||
|
|
||
|
await testServer.server.close();
|
||
|
})();
|
||
|
|
||
|
// Scenario 6: Error recovery and continuation
|
||
|
await (async () => {
|
||
|
scenarioCount++;
|
||
|
console.log(`\nScenario ${scenarioCount}: Testing error recovery and continuation`);
|
||
|
|
||
|
const testServer = await createTestServer({
|
||
|
onConnection: async (socket) => {
|
||
|
console.log(' [Server] Client connected');
|
||
|
socket.write('220 responses.example.com ESMTP\r\n');
|
||
|
|
||
|
let errorCount = 0;
|
||
|
|
||
|
socket.on('data', (data) => {
|
||
|
const command = data.toString().trim();
|
||
|
console.log(` [Server] Received: ${command}`);
|
||
|
|
||
|
if (command.startsWith('EHLO')) {
|
||
|
socket.write('250-responses.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:')) {
|
||
|
const address = command.match(/<(.+)>/)?.[1] || '';
|
||
|
|
||
|
if (address.includes('error1')) {
|
||
|
errorCount++;
|
||
|
socket.write('550 5.1.1 First error - user unknown\r\n');
|
||
|
} else if (address.includes('error2')) {
|
||
|
errorCount++;
|
||
|
socket.write('551 5.1.6 Second error - user not local\r\n');
|
||
|
} else {
|
||
|
socket.write('250 OK\r\n');
|
||
|
}
|
||
|
} else if (command === 'DATA') {
|
||
|
if (errorCount > 0) {
|
||
|
console.log(` [Server] ${errorCount} errors occurred, but continuing`);
|
||
|
}
|
||
|
socket.write('354 Start mail input\r\n');
|
||
|
} else if (command === '.') {
|
||
|
socket.write(`250 OK: Message accepted despite ${errorCount} recipient errors\r\n`);
|
||
|
} else if (command === 'RSET') {
|
||
|
console.log(' [Server] Transaction reset');
|
||
|
errorCount = 0;
|
||
|
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 not recognized\r\n');
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
|
||
|
const smtpClient = createSmtpClient({
|
||
|
host: testServer.hostname,
|
||
|
port: testServer.port,
|
||
|
secure: false
|
||
|
});
|
||
|
|
||
|
// Test with mix of valid and invalid recipients
|
||
|
const email = new plugins.smartmail.Email({
|
||
|
from: 'sender@example.com',
|
||
|
to: ['error1@example.com', 'valid@example.com', 'error2@example.com', 'another-valid@example.com'],
|
||
|
subject: 'Error recovery test',
|
||
|
text: 'Testing error handling and recovery'
|
||
|
});
|
||
|
|
||
|
const result = await smtpClient.sendMail(email);
|
||
|
console.log(` Partial delivery: ${result.accepted?.length || 0} accepted, ${result.rejected?.length || 0} rejected`);
|
||
|
expect(result).toBeDefined();
|
||
|
expect(result.accepted?.length).toBeGreaterThan(0);
|
||
|
expect(result.rejected?.length).toBeGreaterThan(0);
|
||
|
|
||
|
await testServer.server.close();
|
||
|
})();
|
||
|
|
||
|
console.log(`\n${testId}: All ${scenarioCount} response code scenarios tested ✓`);
|
||
|
});
|