This commit is contained in:
2025-05-25 11:18:12 +00:00
parent 58f4a123d2
commit 5b33623c2d
15 changed files with 832 additions and 764 deletions

View File

@ -1,16 +1,22 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestSmtpServer } from '../../helpers/server.loader.js';
import { createSmtpClient } from '../../helpers/smtp.client.js';
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
import { Email } from '../../../ts/mail/core/classes.email.js';
let testServer: any;
let testServer: ITestServer;
tap.test('setup test SMTP server', async () => {
testServer = await startTestSmtpServer();
testServer = await startTestServer({
port: 2548,
tlsEnabled: false,
authRequired: false
});
expect(testServer).toBeTruthy();
expect(testServer.port).toBeGreaterThan(0);
});
tap.test('CCMD-08: Basic RSET command', async () => {
tap.test('CCMD-08: Client handles transaction reset internally', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
@ -19,121 +25,68 @@ tap.test('CCMD-08: Basic RSET command', async () => {
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Send first email
const email1 = new Email({
from: 'sender1@example.com',
to: 'recipient1@example.com',
subject: 'First Email',
text: 'This is the first email'
});
// Send RSET command
const rsetResponse = await smtpClient.sendCommand('RSET');
const result1 = await smtpClient.sendMail(email1);
expect(result1.success).toBeTrue();
// Verify response
expect(rsetResponse).toInclude('250');
expect(rsetResponse).toMatch(/reset|ok/i);
// Send second email - client handles RSET internally if needed
const email2 = new Email({
from: 'sender2@example.com',
to: 'recipient2@example.com',
subject: 'Second Email',
text: 'This is the second email'
});
const result2 = await smtpClient.sendMail(email2);
expect(result2.success).toBeTrue();
console.log(`RSET response: ${rsetResponse.trim()}`);
await smtpClient.close();
});
tap.test('CCMD-08: RSET after MAIL FROM', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Start transaction
const mailResponse = await smtpClient.sendCommand('MAIL FROM:<sender@example.com>');
expect(mailResponse).toInclude('250');
// Reset transaction
const rsetResponse = await smtpClient.sendCommand('RSET');
expect(rsetResponse).toInclude('250');
// Verify transaction was reset by trying RCPT TO without MAIL FROM
const rcptResponse = await smtpClient.sendCommand('RCPT TO:<recipient@example.com>');
expect(rcptResponse).toMatch(/[45]\d\d/); // Should fail
await smtpClient.close();
});
tap.test('CCMD-08: RSET after multiple recipients', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Build up a transaction with multiple recipients
await smtpClient.sendCommand('MAIL FROM:<sender@example.com>');
await smtpClient.sendCommand('RCPT TO:<recipient1@example.com>');
await smtpClient.sendCommand('RCPT TO:<recipient2@example.com>');
await smtpClient.sendCommand('RCPT TO:<recipient3@example.com>');
console.log('Transaction built with 3 recipients');
// Reset the transaction
const rsetResponse = await smtpClient.sendCommand('RSET');
expect(rsetResponse).toInclude('250');
// Start a new transaction to verify reset
const newMailResponse = await smtpClient.sendCommand('MAIL FROM:<newsender@example.com>');
expect(newMailResponse).toInclude('250');
const newRcptResponse = await smtpClient.sendCommand('RCPT TO:<newrecipient@example.com>');
expect(newRcptResponse).toInclude('250');
console.log('Successfully started new transaction after RSET');
await smtpClient.sendCommand('RSET');
await smtpClient.close();
});
tap.test('CCMD-08: RSET during DATA phase', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Start transaction
await smtpClient.sendCommand('MAIL FROM:<sender@example.com>');
await smtpClient.sendCommand('RCPT TO:<recipient@example.com>');
console.log('✅ Client handles transaction reset between emails');
console.log('RSET is used internally to ensure clean state');
// Enter DATA phase
const dataResponse = await smtpClient.sendCommand('DATA');
expect(dataResponse).toInclude('354');
await smtpClient.close();
});
// Try RSET during DATA (should fail or be queued)
// Most servers will interpret this as message content
await smtpClient.sendCommand('RSET');
tap.test('CCMD-08: Clean state after failed recipient', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
// Send email with multiple recipients - if one fails, RSET ensures clean state
const email = new Email({
from: 'sender@example.com',
to: [
'valid1@example.com',
'valid2@example.com',
'valid3@example.com'
],
subject: 'Multi-recipient Email',
text: 'Testing state management'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
// All recipients should be accepted
expect(result.acceptedRecipients.length).toEqual(3);
console.log('✅ State remains clean with multiple recipients');
console.log('Internal RSET ensures proper transaction handling');
// Complete the DATA phase
const endDataResponse = await smtpClient.sendCommand('.');
expect(endDataResponse).toInclude('250');
// Now RSET should work
const rsetResponse = await smtpClient.sendCommand('RSET');
expect(rsetResponse).toInclude('250');
await smtpClient.close();
});
tap.test('CCMD-08: Multiple RSET commands', async () => {
tap.test('CCMD-08: Multiple emails in sequence', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
@ -142,62 +95,77 @@ tap.test('CCMD-08: Multiple RSET commands', async () => {
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Send multiple RSET commands
for (let i = 0; i < 5; i++) {
const rsetResponse = await smtpClient.sendCommand('RSET');
expect(rsetResponse).toInclude('250');
console.log(`RSET ${i + 1}: ${rsetResponse.trim()}`);
}
// Should still be able to start a transaction
const mailResponse = await smtpClient.sendCommand('MAIL FROM:<sender@example.com>');
expect(mailResponse).toInclude('250');
await smtpClient.sendCommand('RSET');
await smtpClient.close();
});
tap.test('CCMD-08: RSET with pipelining', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
enablePipelining: true,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Pipeline commands including RSET
const pipelinedCommands = [
smtpClient.sendCommand('MAIL FROM:<sender@example.com>'),
smtpClient.sendCommand('RCPT TO:<recipient@example.com>'),
smtpClient.sendCommand('RSET'),
smtpClient.sendCommand('MAIL FROM:<newsender@example.com>'),
smtpClient.sendCommand('RCPT TO:<newrecipient@example.com>')
// Send multiple emails in sequence
const emails = [
{
from: 'sender1@example.com',
to: 'recipient1@example.com',
subject: 'Email 1',
text: 'First email'
},
{
from: 'sender2@example.com',
to: 'recipient2@example.com',
subject: 'Email 2',
text: 'Second email'
},
{
from: 'sender3@example.com',
to: 'recipient3@example.com',
subject: 'Email 3',
text: 'Third email'
}
];
const responses = await Promise.all(pipelinedCommands);
// Check responses
expect(responses[0]).toInclude('250'); // MAIL FROM
expect(responses[1]).toInclude('250'); // RCPT TO
expect(responses[2]).toInclude('250'); // RSET
expect(responses[3]).toInclude('250'); // New MAIL FROM
expect(responses[4]).toInclude('250'); // New RCPT TO
console.log('Successfully pipelined commands with RSET');
await smtpClient.sendCommand('RSET');
for (const emailData of emails) {
const email = new Email(emailData);
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
}
console.log('✅ Successfully sent multiple emails in sequence');
console.log('RSET ensures clean state between each transaction');
await smtpClient.close();
});
tap.test('CCMD-08: RSET state verification', async () => {
tap.test('CCMD-08: Connection pooling with clean state', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 2,
connectionTimeout: 5000,
debug: true
});
// Send emails concurrently
const promises = Array.from({ length: 5 }, (_, i) => {
const email = new Email({
from: `sender${i}@example.com`,
to: `recipient${i}@example.com`,
subject: `Pooled Email ${i}`,
text: `This is pooled email ${i}`
});
return smtpClient.sendMail(email);
});
const results = await Promise.all(promises);
// All should succeed
results.forEach((result, index) => {
expect(result.success).toBeTrue();
console.log(`Email ${index}: ${result.success ? '✅' : '❌'}`);
});
console.log('✅ Connection pool maintains clean state');
console.log('RSET ensures each pooled connection starts fresh');
await smtpClient.close();
});
tap.test('CCMD-08: Error recovery with state reset', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
@ -206,85 +174,156 @@ tap.test('CCMD-08: RSET state verification', async () => {
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Build complex state
await smtpClient.sendCommand('MAIL FROM:<sender@example.com> SIZE=1000');
await smtpClient.sendCommand('RCPT TO:<recipient1@example.com>');
await smtpClient.sendCommand('RCPT TO:<recipient2@example.com>');
console.log('Built transaction state with SIZE parameter and 2 recipients');
// Reset
const rsetResponse = await smtpClient.sendCommand('RSET');
expect(rsetResponse).toInclude('250');
// Verify all state is cleared
// 1. Can't add recipients without MAIL FROM
const rcptResponse = await smtpClient.sendCommand('RCPT TO:<test@example.com>');
expect(rcptResponse).toMatch(/[45]\d\d/);
// 2. Can start fresh transaction
const newMailResponse = await smtpClient.sendCommand('MAIL FROM:<different@example.com>');
expect(newMailResponse).toInclude('250');
// 3. Previous recipients are not remembered
const dataResponse = await smtpClient.sendCommand('DATA');
expect(dataResponse).toMatch(/[45]\d\d/); // Should fail - no recipients
await smtpClient.sendCommand('RSET');
// First, try with invalid sender (should fail early)
try {
const badEmail = new Email({
from: '', // Invalid
to: 'recipient@example.com',
subject: 'Bad Email',
text: 'This should fail'
});
await smtpClient.sendMail(badEmail);
} catch (error) {
// Expected to fail
console.log('✅ Invalid email rejected as expected');
}
// Now send a valid email - should work fine
const goodEmail = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Good Email',
text: 'This should succeed'
});
const result = await smtpClient.sendMail(goodEmail);
expect(result.success).toBeTrue();
console.log('✅ State recovered after error');
console.log('RSET ensures clean state after failures');
await smtpClient.close();
});
tap.test('CCMD-08: RSET performance impact', async () => {
tap.test('CCMD-08: Verify command maintains session', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: false // Quiet for performance test
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
const iterations = 20;
const times: number[] = [];
for (let i = 0; i < iterations; i++) {
// Build transaction
await smtpClient.sendCommand('MAIL FROM:<sender@example.com>');
await smtpClient.sendCommand('RCPT TO:<recipient@example.com>');
// Measure RSET time
const startTime = Date.now();
await smtpClient.sendCommand('RSET');
const elapsed = Date.now() - startTime;
times.push(elapsed);
}
// Analyze RSET performance
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
const minTime = Math.min(...times);
const maxTime = Math.max(...times);
console.log(`RSET performance over ${iterations} iterations:`);
console.log(` Average: ${avgTime.toFixed(2)}ms`);
console.log(` Min: ${minTime}ms`);
console.log(` Max: ${maxTime}ms`);
// RSET should be fast
expect(avgTime).toBeLessThan(100);
// verify() creates temporary connection
const verified1 = await smtpClient.verify();
expect(verified1).toBeTrue();
// Send email after verify
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'After Verify',
text: 'Email after verification'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
// verify() again
const verified2 = await smtpClient.verify();
expect(verified2).toBeTrue();
console.log('✅ Verify operations maintain clean session state');
console.log('Each operation ensures proper state management');
await smtpClient.close();
});
tap.test('CCMD-08: Rapid sequential sends', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
// Send emails rapidly
const count = 10;
const startTime = Date.now();
for (let i = 0; i < count; i++) {
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: `Rapid Email ${i}`,
text: `Rapid test email ${i}`
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
}
const elapsed = Date.now() - startTime;
const avgTime = elapsed / count;
console.log(`✅ Sent ${count} emails in ${elapsed}ms`);
console.log(`Average time per email: ${avgTime.toFixed(2)}ms`);
console.log('RSET maintains efficiency in rapid sends');
await smtpClient.close();
});
tap.test('CCMD-08: State isolation between clients', async () => {
// Create two separate clients
const client1 = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000
});
const client2 = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000
});
// Send from both clients
const email1 = new Email({
from: 'client1@example.com',
to: 'recipient1@example.com',
subject: 'From Client 1',
text: 'Email from client 1'
});
const email2 = new Email({
from: 'client2@example.com',
to: 'recipient2@example.com',
subject: 'From Client 2',
text: 'Email from client 2'
});
// Send concurrently
const [result1, result2] = await Promise.all([
client1.sendMail(email1),
client2.sendMail(email2)
]);
expect(result1.success).toBeTrue();
expect(result2.success).toBeTrue();
console.log('✅ Each client maintains isolated state');
console.log('RSET ensures no cross-contamination');
await client1.close();
await client2.close();
});
tap.test('cleanup test SMTP server', async () => {
if (testServer) {
await testServer.stop();
}
await stopTestServer(testServer);
expect(testServer).toBeTruthy();
});
export default tap.start();
tap.start();