703 lines
25 KiB
TypeScript
703 lines
25 KiB
TypeScript
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-05: should comply with SMTP state machine (RFC 5321)', async (tools) => {
|
|
const testId = 'CRFC-05-state-machine';
|
|
console.log(`\n${testId}: Testing SMTP state machine compliance...`);
|
|
|
|
let scenarioCount = 0;
|
|
|
|
// Scenario 1: Initial state and greeting
|
|
await (async () => {
|
|
scenarioCount++;
|
|
console.log(`\nScenario ${scenarioCount}: Testing initial state and greeting`);
|
|
|
|
const testServer = await createTestServer({
|
|
onConnection: async (socket) => {
|
|
console.log(' [Server] Client connected - Initial state');
|
|
|
|
let state = 'initial';
|
|
|
|
// Send greeting immediately upon connection
|
|
socket.write('220 statemachine.example.com ESMTP Service ready\r\n');
|
|
state = 'greeting-sent';
|
|
console.log(' [Server] State: initial -> greeting-sent');
|
|
|
|
socket.on('data', (data) => {
|
|
const command = data.toString().trim();
|
|
console.log(` [Server] State: ${state}, Received: ${command}`);
|
|
|
|
if (state === 'greeting-sent') {
|
|
if (command.startsWith('EHLO') || command.startsWith('HELO')) {
|
|
socket.write('250 statemachine.example.com\r\n');
|
|
state = 'ready';
|
|
console.log(' [Server] State: greeting-sent -> 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');
|
|
}
|
|
} else if (state === 'ready') {
|
|
if (command.startsWith('MAIL FROM:')) {
|
|
socket.write('250 OK\r\n');
|
|
state = 'mail';
|
|
console.log(' [Server] State: ready -> mail');
|
|
} else if (command.startsWith('EHLO') || command.startsWith('HELO')) {
|
|
socket.write('250 statemachine.example.com\r\n');
|
|
// Stay in ready state
|
|
} else if (command === 'RSET' || command === 'NOOP') {
|
|
socket.write('250 OK\r\n');
|
|
// Stay in ready state
|
|
} 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 = createTestSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false
|
|
});
|
|
|
|
// Just establish connection and send EHLO
|
|
try {
|
|
await smtpClient.verify();
|
|
console.log(' Initial state transition (connect -> EHLO) successful');
|
|
} catch (error) {
|
|
console.log(` Connection/EHLO failed: ${error.message}`);
|
|
}
|
|
|
|
await testServer.server.close();
|
|
})();
|
|
|
|
// Scenario 2: Transaction state machine
|
|
await (async () => {
|
|
scenarioCount++;
|
|
console.log(`\nScenario ${scenarioCount}: Testing transaction state machine`);
|
|
|
|
const testServer = await createTestServer({
|
|
onConnection: async (socket) => {
|
|
console.log(' [Server] Client connected');
|
|
socket.write('220 statemachine.example.com ESMTP\r\n');
|
|
|
|
let state = 'ready';
|
|
|
|
socket.on('data', (data) => {
|
|
const command = data.toString().trim();
|
|
console.log(` [Server] State: ${state}, Command: ${command}`);
|
|
|
|
switch (state) {
|
|
case 'ready':
|
|
if (command.startsWith('EHLO')) {
|
|
socket.write('250 statemachine.example.com\r\n');
|
|
// Stay in ready
|
|
} else if (command.startsWith('MAIL FROM:')) {
|
|
socket.write('250 OK\r\n');
|
|
state = 'mail';
|
|
console.log(' [Server] State: ready -> mail');
|
|
} 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');
|
|
}
|
|
break;
|
|
|
|
case 'mail':
|
|
if (command.startsWith('RCPT TO:')) {
|
|
socket.write('250 OK\r\n');
|
|
state = 'rcpt';
|
|
console.log(' [Server] State: mail -> rcpt');
|
|
} else if (command === 'RSET') {
|
|
socket.write('250 OK\r\n');
|
|
state = 'ready';
|
|
console.log(' [Server] State: mail -> ready (RSET)');
|
|
} 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');
|
|
}
|
|
break;
|
|
|
|
case 'rcpt':
|
|
if (command.startsWith('RCPT TO:')) {
|
|
socket.write('250 OK\r\n');
|
|
// Stay in rcpt (can have multiple recipients)
|
|
} else if (command === 'DATA') {
|
|
socket.write('354 Start mail input\r\n');
|
|
state = 'data';
|
|
console.log(' [Server] State: rcpt -> data');
|
|
} else if (command === 'RSET') {
|
|
socket.write('250 OK\r\n');
|
|
state = 'ready';
|
|
console.log(' [Server] State: rcpt -> ready (RSET)');
|
|
} 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');
|
|
}
|
|
break;
|
|
|
|
case 'data':
|
|
if (command === '.') {
|
|
socket.write('250 OK\r\n');
|
|
state = 'ready';
|
|
console.log(' [Server] State: data -> ready (message complete)');
|
|
} else if (command === 'QUIT') {
|
|
// QUIT is not allowed during DATA
|
|
socket.write('503 5.5.1 Bad sequence of commands\r\n');
|
|
}
|
|
// All other input during DATA is message content
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
const smtpClient = createTestSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false
|
|
});
|
|
|
|
const email = new Email({
|
|
from: 'sender@example.com',
|
|
to: ['recipient1@example.com', 'recipient2@example.com'],
|
|
subject: 'State machine test',
|
|
text: 'Testing SMTP transaction state machine'
|
|
});
|
|
|
|
const result = await smtpClient.sendMail(email);
|
|
console.log(' Complete transaction state sequence successful');
|
|
expect(result).toBeDefined();
|
|
expect(result.messageId).toBeDefined();
|
|
|
|
await testServer.server.close();
|
|
})();
|
|
|
|
// Scenario 3: Invalid state transitions
|
|
await (async () => {
|
|
scenarioCount++;
|
|
console.log(`\nScenario ${scenarioCount}: Testing invalid state transitions`);
|
|
|
|
const testServer = await createTestServer({
|
|
onConnection: async (socket) => {
|
|
console.log(' [Server] Client connected');
|
|
socket.write('220 statemachine.example.com ESMTP\r\n');
|
|
|
|
let state = 'ready';
|
|
|
|
socket.on('data', (data) => {
|
|
const command = data.toString().trim();
|
|
console.log(` [Server] State: ${state}, Command: ${command}`);
|
|
|
|
// Strictly enforce state machine
|
|
switch (state) {
|
|
case 'ready':
|
|
if (command.startsWith('EHLO') || command.startsWith('HELO')) {
|
|
socket.write('250 statemachine.example.com\r\n');
|
|
} else if (command.startsWith('MAIL FROM:')) {
|
|
socket.write('250 OK\r\n');
|
|
state = 'mail';
|
|
} else if (command === 'RSET' || command === 'NOOP') {
|
|
socket.write('250 OK\r\n');
|
|
} else if (command === 'QUIT') {
|
|
socket.write('221 Bye\r\n');
|
|
socket.end();
|
|
} else if (command.startsWith('RCPT TO:')) {
|
|
console.log(' [Server] RCPT TO without MAIL FROM');
|
|
socket.write('503 5.5.1 Need MAIL command first\r\n');
|
|
} else if (command === 'DATA') {
|
|
console.log(' [Server] DATA without MAIL FROM and RCPT TO');
|
|
socket.write('503 5.5.1 Need MAIL and RCPT commands first\r\n');
|
|
} else {
|
|
socket.write('503 5.5.1 Bad sequence of commands\r\n');
|
|
}
|
|
break;
|
|
|
|
case 'mail':
|
|
if (command.startsWith('RCPT TO:')) {
|
|
socket.write('250 OK\r\n');
|
|
state = 'rcpt';
|
|
} else if (command.startsWith('MAIL FROM:')) {
|
|
console.log(' [Server] Second MAIL FROM without RSET');
|
|
socket.write('503 5.5.1 Sender already specified\r\n');
|
|
} else if (command === 'DATA') {
|
|
console.log(' [Server] DATA without RCPT TO');
|
|
socket.write('503 5.5.1 Need RCPT command first\r\n');
|
|
} else if (command === 'RSET') {
|
|
socket.write('250 OK\r\n');
|
|
state = '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');
|
|
}
|
|
break;
|
|
|
|
case 'rcpt':
|
|
if (command.startsWith('RCPT TO:')) {
|
|
socket.write('250 OK\r\n');
|
|
} else if (command === 'DATA') {
|
|
socket.write('354 Start mail input\r\n');
|
|
state = 'data';
|
|
} else if (command.startsWith('MAIL FROM:')) {
|
|
console.log(' [Server] MAIL FROM after RCPT TO without RSET');
|
|
socket.write('503 5.5.1 Sender already specified\r\n');
|
|
} else if (command === 'RSET') {
|
|
socket.write('250 OK\r\n');
|
|
state = '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');
|
|
}
|
|
break;
|
|
|
|
case 'data':
|
|
if (command === '.') {
|
|
socket.write('250 OK\r\n');
|
|
state = 'ready';
|
|
} else if (command.startsWith('MAIL FROM:') ||
|
|
command.startsWith('RCPT TO:') ||
|
|
command === 'RSET') {
|
|
console.log(' [Server] SMTP command during DATA mode');
|
|
socket.write('503 5.5.1 Commands not allowed during data transfer\r\n');
|
|
}
|
|
// During DATA, most input is treated as message content
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// We'll create a custom client to send invalid command sequences
|
|
const testCases = [
|
|
{
|
|
name: 'RCPT without MAIL',
|
|
commands: ['EHLO client.example.com', 'RCPT TO:<test@example.com>'],
|
|
expectError: true
|
|
},
|
|
{
|
|
name: 'DATA without RCPT',
|
|
commands: ['EHLO client.example.com', 'MAIL FROM:<sender@example.com>', 'DATA'],
|
|
expectError: true
|
|
},
|
|
{
|
|
name: 'Double MAIL FROM',
|
|
commands: ['EHLO client.example.com', 'MAIL FROM:<sender1@example.com>', 'MAIL FROM:<sender2@example.com>'],
|
|
expectError: true
|
|
}
|
|
];
|
|
|
|
for (const testCase of testCases) {
|
|
console.log(` Testing: ${testCase.name}`);
|
|
|
|
try {
|
|
// Create simple socket connection for manual command testing
|
|
const net = await import('net');
|
|
const client = net.createConnection(testServer.port, testServer.hostname);
|
|
|
|
let responseCount = 0;
|
|
let errorReceived = false;
|
|
|
|
client.on('data', (data) => {
|
|
const response = data.toString();
|
|
console.log(` Response: ${response.trim()}`);
|
|
|
|
if (response.startsWith('5')) {
|
|
errorReceived = true;
|
|
}
|
|
|
|
responseCount++;
|
|
|
|
if (responseCount <= testCase.commands.length) {
|
|
const command = testCase.commands[responseCount - 1];
|
|
if (command) {
|
|
setTimeout(() => {
|
|
console.log(` Sending: ${command}`);
|
|
client.write(command + '\r\n');
|
|
}, 100);
|
|
}
|
|
} else {
|
|
client.write('QUIT\r\n');
|
|
client.end();
|
|
}
|
|
});
|
|
|
|
await new Promise((resolve, reject) => {
|
|
client.on('end', () => {
|
|
if (testCase.expectError && errorReceived) {
|
|
console.log(` ✓ Expected error received`);
|
|
} else if (!testCase.expectError && !errorReceived) {
|
|
console.log(` ✓ No error as expected`);
|
|
} else {
|
|
console.log(` ✗ Unexpected result`);
|
|
}
|
|
resolve(void 0);
|
|
});
|
|
|
|
client.on('error', reject);
|
|
|
|
// Start with greeting response
|
|
setTimeout(() => {
|
|
if (testCase.commands.length > 0) {
|
|
console.log(` Sending: ${testCase.commands[0]}`);
|
|
client.write(testCase.commands[0] + '\r\n');
|
|
}
|
|
}, 100);
|
|
});
|
|
|
|
} catch (error) {
|
|
console.log(` Error testing ${testCase.name}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
await testServer.server.close();
|
|
})();
|
|
|
|
// Scenario 4: RSET command state transitions
|
|
await (async () => {
|
|
scenarioCount++;
|
|
console.log(`\nScenario ${scenarioCount}: Testing RSET command state transitions`);
|
|
|
|
const testServer = await createTestServer({
|
|
onConnection: async (socket) => {
|
|
console.log(' [Server] Client connected');
|
|
socket.write('220 statemachine.example.com ESMTP\r\n');
|
|
|
|
let state = 'ready';
|
|
|
|
socket.on('data', (data) => {
|
|
const command = data.toString().trim();
|
|
console.log(` [Server] State: ${state}, Command: ${command}`);
|
|
|
|
if (command.startsWith('EHLO')) {
|
|
socket.write('250 statemachine.example.com\r\n');
|
|
state = 'ready';
|
|
} else if (command.startsWith('MAIL FROM:')) {
|
|
socket.write('250 OK\r\n');
|
|
state = 'mail';
|
|
} else if (command.startsWith('RCPT TO:')) {
|
|
if (state === 'mail' || state === 'rcpt') {
|
|
socket.write('250 OK\r\n');
|
|
state = 'rcpt';
|
|
} else {
|
|
socket.write('503 5.5.1 Bad sequence of commands\r\n');
|
|
}
|
|
} else if (command === 'RSET') {
|
|
console.log(` [Server] RSET from state: ${state} -> ready`);
|
|
socket.write('250 OK\r\n');
|
|
state = 'ready';
|
|
} else if (command === 'DATA') {
|
|
if (state === 'rcpt') {
|
|
socket.write('354 Start mail input\r\n');
|
|
state = 'data';
|
|
} else {
|
|
socket.write('503 5.5.1 Bad sequence of commands\r\n');
|
|
}
|
|
} else if (command === '.') {
|
|
if (state === 'data') {
|
|
socket.write('250 OK\r\n');
|
|
state = 'ready';
|
|
}
|
|
} else if (command === 'QUIT') {
|
|
socket.write('221 Bye\r\n');
|
|
socket.end();
|
|
} else if (command === 'NOOP') {
|
|
socket.write('250 OK\r\n');
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
const smtpClient = createTestSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false
|
|
});
|
|
|
|
// Test RSET at various points in transaction
|
|
console.log(' Testing RSET from different states...');
|
|
|
|
// We'll manually test RSET behavior
|
|
const net = await import('net');
|
|
const client = net.createConnection(testServer.port, testServer.hostname);
|
|
|
|
const commands = [
|
|
'EHLO client.example.com', // -> ready
|
|
'MAIL FROM:<sender@example.com>', // -> mail
|
|
'RSET', // -> ready (reset from mail state)
|
|
'MAIL FROM:<sender2@example.com>', // -> mail
|
|
'RCPT TO:<rcpt1@example.com>', // -> rcpt
|
|
'RCPT TO:<rcpt2@example.com>', // -> rcpt (multiple recipients)
|
|
'RSET', // -> ready (reset from rcpt state)
|
|
'MAIL FROM:<sender3@example.com>', // -> mail (fresh transaction)
|
|
'RCPT TO:<rcpt3@example.com>', // -> rcpt
|
|
'DATA', // -> data
|
|
'.', // -> ready (complete transaction)
|
|
'QUIT'
|
|
];
|
|
|
|
let commandIndex = 0;
|
|
|
|
client.on('data', (data) => {
|
|
const response = data.toString().trim();
|
|
console.log(` Response: ${response}`);
|
|
|
|
if (commandIndex < commands.length) {
|
|
setTimeout(() => {
|
|
const command = commands[commandIndex];
|
|
console.log(` Sending: ${command}`);
|
|
if (command === 'DATA') {
|
|
client.write(command + '\r\n');
|
|
// Send message content immediately after DATA
|
|
setTimeout(() => {
|
|
client.write('Subject: RSET test\r\n\r\nTesting RSET state transitions.\r\n.\r\n');
|
|
}, 100);
|
|
} else {
|
|
client.write(command + '\r\n');
|
|
}
|
|
commandIndex++;
|
|
}, 100);
|
|
} else {
|
|
client.end();
|
|
}
|
|
});
|
|
|
|
await new Promise((resolve, reject) => {
|
|
client.on('end', () => {
|
|
console.log(' RSET state transitions completed successfully');
|
|
resolve(void 0);
|
|
});
|
|
client.on('error', reject);
|
|
});
|
|
|
|
await testServer.server.close();
|
|
})();
|
|
|
|
// Scenario 5: Connection state persistence
|
|
await (async () => {
|
|
scenarioCount++;
|
|
console.log(`\nScenario ${scenarioCount}: Testing connection state persistence`);
|
|
|
|
const testServer = await createTestServer({
|
|
onConnection: async (socket) => {
|
|
console.log(' [Server] Client connected');
|
|
socket.write('220 statemachine.example.com ESMTP\r\n');
|
|
|
|
let state = 'ready';
|
|
let messageCount = 0;
|
|
|
|
socket.on('data', (data) => {
|
|
const command = data.toString().trim();
|
|
|
|
if (command.startsWith('EHLO')) {
|
|
socket.write('250-statemachine.example.com\r\n');
|
|
socket.write('250 PIPELINING\r\n');
|
|
state = 'ready';
|
|
} else if (command.startsWith('MAIL FROM:')) {
|
|
if (state === 'ready') {
|
|
socket.write('250 OK\r\n');
|
|
state = 'mail';
|
|
} else {
|
|
socket.write('503 5.5.1 Bad sequence\r\n');
|
|
}
|
|
} else if (command.startsWith('RCPT TO:')) {
|
|
if (state === 'mail' || state === 'rcpt') {
|
|
socket.write('250 OK\r\n');
|
|
state = 'rcpt';
|
|
} else {
|
|
socket.write('503 5.5.1 Bad sequence\r\n');
|
|
}
|
|
} else if (command === 'DATA') {
|
|
if (state === 'rcpt') {
|
|
socket.write('354 Start mail input\r\n');
|
|
state = 'data';
|
|
} else {
|
|
socket.write('503 5.5.1 Bad sequence\r\n');
|
|
}
|
|
} else if (command === '.') {
|
|
if (state === 'data') {
|
|
messageCount++;
|
|
console.log(` [Server] Message ${messageCount} completed`);
|
|
socket.write(`250 OK: Message ${messageCount} accepted\r\n`);
|
|
state = 'ready';
|
|
}
|
|
} else if (command === 'QUIT') {
|
|
console.log(` [Server] Session ended after ${messageCount} messages`);
|
|
socket.write('221 Bye\r\n');
|
|
socket.end();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
const smtpClient = createTestSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false,
|
|
pool: true,
|
|
maxConnections: 1
|
|
});
|
|
|
|
// Send multiple emails through same connection
|
|
for (let i = 1; i <= 3; i++) {
|
|
const email = new Email({
|
|
from: 'sender@example.com',
|
|
to: [`recipient${i}@example.com`],
|
|
subject: `Persistence test ${i}`,
|
|
text: `Testing connection state persistence - message ${i}`
|
|
});
|
|
|
|
const result = await smtpClient.sendMail(email);
|
|
console.log(` Message ${i} sent successfully`);
|
|
expect(result).toBeDefined();
|
|
expect(result.response).toContain(`Message ${i}`);
|
|
}
|
|
|
|
// Close the pooled connection
|
|
await smtpClient.close();
|
|
await testServer.server.close();
|
|
})();
|
|
|
|
// Scenario 6: Error state recovery
|
|
await (async () => {
|
|
scenarioCount++;
|
|
console.log(`\nScenario ${scenarioCount}: Testing error state recovery`);
|
|
|
|
const testServer = await createTestServer({
|
|
onConnection: async (socket) => {
|
|
console.log(' [Server] Client connected');
|
|
socket.write('220 statemachine.example.com ESMTP\r\n');
|
|
|
|
let state = 'ready';
|
|
let errorCount = 0;
|
|
|
|
socket.on('data', (data) => {
|
|
const command = data.toString().trim();
|
|
console.log(` [Server] State: ${state}, Command: ${command}`);
|
|
|
|
if (command.startsWith('EHLO')) {
|
|
socket.write('250 statemachine.example.com\r\n');
|
|
state = 'ready';
|
|
errorCount = 0; // Reset error count on new session
|
|
} else if (command.startsWith('MAIL FROM:')) {
|
|
const address = command.match(/<(.+)>/)?.[1] || '';
|
|
if (address.includes('error')) {
|
|
errorCount++;
|
|
console.log(` [Server] Error ${errorCount} - invalid sender`);
|
|
socket.write('550 5.1.8 Invalid sender address\r\n');
|
|
// State remains ready after error
|
|
} else {
|
|
socket.write('250 OK\r\n');
|
|
state = 'mail';
|
|
}
|
|
} else if (command.startsWith('RCPT TO:')) {
|
|
if (state === 'mail' || state === 'rcpt') {
|
|
const address = command.match(/<(.+)>/)?.[1] || '';
|
|
if (address.includes('error')) {
|
|
errorCount++;
|
|
console.log(` [Server] Error ${errorCount} - invalid recipient`);
|
|
socket.write('550 5.1.1 User unknown\r\n');
|
|
// State remains the same after recipient error
|
|
} else {
|
|
socket.write('250 OK\r\n');
|
|
state = 'rcpt';
|
|
}
|
|
} else {
|
|
socket.write('503 5.5.1 Bad sequence\r\n');
|
|
}
|
|
} else if (command === 'DATA') {
|
|
if (state === 'rcpt') {
|
|
socket.write('354 Start mail input\r\n');
|
|
state = 'data';
|
|
} else {
|
|
socket.write('503 5.5.1 Bad sequence\r\n');
|
|
}
|
|
} else if (command === '.') {
|
|
if (state === 'data') {
|
|
socket.write('250 OK\r\n');
|
|
state = 'ready';
|
|
}
|
|
} else if (command === 'RSET') {
|
|
console.log(` [Server] RSET - recovering from errors (${errorCount} errors so far)`);
|
|
socket.write('250 OK\r\n');
|
|
state = 'ready';
|
|
} else if (command === 'QUIT') {
|
|
console.log(` [Server] Session ended with ${errorCount} total errors`);
|
|
socket.write('221 Bye\r\n');
|
|
socket.end();
|
|
} else {
|
|
socket.write('500 5.5.1 Command not recognized\r\n');
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
const smtpClient = createTestSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false
|
|
});
|
|
|
|
// Test recovery from various errors
|
|
const testEmails = [
|
|
{
|
|
from: 'error@example.com', // Will cause sender error
|
|
to: ['valid@example.com'],
|
|
desc: 'invalid sender'
|
|
},
|
|
{
|
|
from: 'valid@example.com',
|
|
to: ['error@example.com', 'valid@example.com'], // Mixed valid/invalid recipients
|
|
desc: 'mixed recipients'
|
|
},
|
|
{
|
|
from: 'valid@example.com',
|
|
to: ['valid@example.com'],
|
|
desc: 'valid email after errors'
|
|
}
|
|
];
|
|
|
|
for (const testEmail of testEmails) {
|
|
console.log(` Testing ${testEmail.desc}...`);
|
|
|
|
const email = new Email({
|
|
from: testEmail.from,
|
|
to: testEmail.to,
|
|
subject: `Error recovery test: ${testEmail.desc}`,
|
|
text: `Testing error state recovery with ${testEmail.desc}`
|
|
});
|
|
|
|
try {
|
|
const result = await smtpClient.sendMail(email);
|
|
console.log(` ${testEmail.desc}: Success`);
|
|
if (result.rejected && result.rejected.length > 0) {
|
|
console.log(` Rejected: ${result.rejected.length} recipients`);
|
|
}
|
|
} catch (error) {
|
|
console.log(` ${testEmail.desc}: Failed as expected - ${error.message}`);
|
|
}
|
|
}
|
|
|
|
await testServer.server.close();
|
|
})();
|
|
|
|
console.log(`\n${testId}: All ${scenarioCount} state machine scenarios tested ✓`);
|
|
});
|
|
|
|
tap.start(); |