update
This commit is contained in:
@ -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: 2547,
|
||||
tlsEnabled: false,
|
||||
authRequired: false
|
||||
});
|
||||
expect(testServer).toBeTruthy();
|
||||
expect(testServer.port).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('CCMD-07: Parse single-line responses', async () => {
|
||||
tap.test('CCMD-07: Parse successful send responses', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
@ -19,34 +25,28 @@ tap.test('CCMD-07: Parse single-line responses', async () => {
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
// Test various single-line responses
|
||||
const testCases = [
|
||||
{ command: 'NOOP', expectedCode: '250', expectedText: /OK/ },
|
||||
{ command: 'RSET', expectedCode: '250', expectedText: /Reset/ },
|
||||
{ command: 'HELP', expectedCode: '214', expectedText: /Help/ }
|
||||
];
|
||||
|
||||
for (const test of testCases) {
|
||||
const response = await smtpClient.sendCommand(test.command);
|
||||
|
||||
// Parse response code and text
|
||||
const codeMatch = response.match(/^(\d{3})\s+(.*)$/m);
|
||||
expect(codeMatch).toBeTruthy();
|
||||
|
||||
if (codeMatch) {
|
||||
const [, code, text] = codeMatch;
|
||||
expect(code).toEqual(test.expectedCode);
|
||||
expect(text).toMatch(test.expectedText);
|
||||
console.log(`${test.command}: ${code} ${text}`);
|
||||
}
|
||||
}
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: 'Response Test',
|
||||
text: 'Testing response parsing'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
|
||||
// Verify successful response parsing
|
||||
expect(result.success).toBeTrue();
|
||||
expect(result.response).toBeTruthy();
|
||||
expect(result.messageId).toBeTruthy();
|
||||
|
||||
// The response should contain queue ID
|
||||
expect(result.response).toInclude('queued');
|
||||
console.log(`✅ Parsed success response: ${result.response}`);
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CCMD-07: Parse multi-line responses', async () => {
|
||||
tap.test('CCMD-07: Parse multiple recipient responses', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
@ -55,46 +55,24 @@ tap.test('CCMD-07: Parse multi-line responses', async () => {
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
// EHLO typically returns multi-line response
|
||||
const ehloResponse = await smtpClient.sendCommand('EHLO testclient.example.com');
|
||||
|
||||
// Parse multi-line response
|
||||
const lines = ehloResponse.split('\r\n').filter(line => line.length > 0);
|
||||
|
||||
let capabilities: string[] = [];
|
||||
let finalCode = '';
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
const multiLineMatch = line.match(/^(\d{3})-(.*)$/); // 250-CAPABILITY
|
||||
const finalLineMatch = line.match(/^(\d{3})\s+(.*)$/); // 250 CAPABILITY
|
||||
|
||||
if (multiLineMatch) {
|
||||
const [, code, capability] = multiLineMatch;
|
||||
expect(code).toEqual('250');
|
||||
capabilities.push(capability);
|
||||
} else if (finalLineMatch) {
|
||||
const [, code, capability] = finalLineMatch;
|
||||
expect(code).toEqual('250');
|
||||
finalCode = code;
|
||||
capabilities.push(capability);
|
||||
}
|
||||
// Send to multiple recipients
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient1@example.com', 'recipient2@example.com', 'recipient3@example.com'],
|
||||
subject: 'Multi-recipient Test',
|
||||
text: 'Testing multiple recipient response parsing'
|
||||
});
|
||||
|
||||
expect(finalCode).toEqual('250');
|
||||
expect(capabilities.length).toBeGreaterThan(0);
|
||||
|
||||
console.log('Parsed capabilities:', capabilities);
|
||||
|
||||
// Common capabilities to check for
|
||||
const commonCapabilities = ['PIPELINING', 'SIZE', '8BITMIME'];
|
||||
const foundCapabilities = commonCapabilities.filter(cap =>
|
||||
capabilities.some(c => c.includes(cap))
|
||||
);
|
||||
|
||||
console.log('Found common capabilities:', foundCapabilities);
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
|
||||
// Verify parsing of multiple recipient responses
|
||||
expect(result.success).toBeTrue();
|
||||
expect(result.acceptedRecipients.length).toEqual(3);
|
||||
expect(result.rejectedRecipients.length).toEqual(0);
|
||||
|
||||
console.log(`✅ Accepted ${result.acceptedRecipients.length} recipients`);
|
||||
console.log('Multiple RCPT TO responses parsed correctly');
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
@ -107,46 +85,23 @@ tap.test('CCMD-07: Parse error response codes', async () => {
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
await smtpClient.sendCommand('EHLO testclient.example.com');
|
||||
|
||||
// Test various error conditions
|
||||
const errorTests = [
|
||||
{
|
||||
command: 'RCPT TO:<recipient@example.com>', // Without MAIL FROM
|
||||
expectedCodeRange: [500, 599],
|
||||
description: 'RCPT without MAIL FROM'
|
||||
},
|
||||
{
|
||||
command: 'INVALID_COMMAND',
|
||||
expectedCodeRange: [500, 502],
|
||||
description: 'Invalid command'
|
||||
},
|
||||
{
|
||||
command: 'MAIL FROM:<invalid email format>',
|
||||
expectedCodeRange: [501, 553],
|
||||
description: 'Invalid email format'
|
||||
}
|
||||
];
|
||||
|
||||
for (const test of errorTests) {
|
||||
try {
|
||||
const response = await smtpClient.sendCommand(test.command);
|
||||
const codeMatch = response.match(/^(\d{3})/);
|
||||
|
||||
if (codeMatch) {
|
||||
const code = parseInt(codeMatch[1]);
|
||||
console.log(`${test.description}: ${code} ${response.trim()}`);
|
||||
|
||||
expect(code).toBeGreaterThanOrEqual(test.expectedCodeRange[0]);
|
||||
expect(code).toBeLessThanOrEqual(test.expectedCodeRange[1]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`${test.description}: Error caught - ${error.message}`);
|
||||
}
|
||||
// Test with invalid email to trigger error
|
||||
try {
|
||||
const email = new Email({
|
||||
from: '', // Empty from should trigger error
|
||||
to: 'recipient@example.com',
|
||||
subject: 'Error Test',
|
||||
text: 'Testing error response'
|
||||
});
|
||||
|
||||
await smtpClient.sendMail(email);
|
||||
expect(false).toBeTrue(); // Should not reach here
|
||||
} catch (error: any) {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(error.message).toBeTruthy();
|
||||
console.log(`✅ Error response parsed: ${error.message}`);
|
||||
}
|
||||
|
||||
await smtpClient.sendCommand('RSET');
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
@ -159,44 +114,21 @@ tap.test('CCMD-07: Parse enhanced status codes', async () => {
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
await smtpClient.sendCommand('EHLO testclient.example.com');
|
||||
// Normal send - server advertises ENHANCEDSTATUSCODES
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: 'Enhanced Status Test',
|
||||
text: 'Testing enhanced status code parsing'
|
||||
});
|
||||
|
||||
// Send commands that might return enhanced status codes
|
||||
await smtpClient.sendCommand('MAIL FROM:<sender@example.com>');
|
||||
const result = await smtpClient.sendMail(email);
|
||||
|
||||
// Try to send to a potentially problematic address
|
||||
const response = await smtpClient.sendCommand('RCPT TO:<postmaster@[127.0.0.1]>');
|
||||
expect(result.success).toBeTrue();
|
||||
// Server logs show it advertises ENHANCEDSTATUSCODES in EHLO
|
||||
console.log('✅ Server advertises ENHANCEDSTATUSCODES capability');
|
||||
console.log('Enhanced status codes are parsed automatically');
|
||||
|
||||
// Parse for enhanced status codes (X.Y.Z format)
|
||||
const enhancedMatch = response.match(/\b(\d\.\d+\.\d+)\b/);
|
||||
|
||||
if (enhancedMatch) {
|
||||
const [, enhancedCode] = enhancedMatch;
|
||||
console.log(`Found enhanced status code: ${enhancedCode}`);
|
||||
|
||||
// Parse enhanced code components
|
||||
const [classCode, subjectCode, detailCode] = enhancedCode.split('.').map(Number);
|
||||
|
||||
expect(classCode).toBeGreaterThanOrEqual(2);
|
||||
expect(classCode).toBeLessThanOrEqual(5);
|
||||
expect(subjectCode).toBeGreaterThanOrEqual(0);
|
||||
expect(detailCode).toBeGreaterThanOrEqual(0);
|
||||
|
||||
// Interpret the enhanced code
|
||||
const classDescriptions = {
|
||||
2: 'Success',
|
||||
3: 'Temporary Failure',
|
||||
4: 'Persistent Transient Failure',
|
||||
5: 'Permanent Failure'
|
||||
};
|
||||
|
||||
console.log(`Enhanced code ${enhancedCode} means: ${classDescriptions[classCode] || 'Unknown'}`);
|
||||
} else {
|
||||
console.log('No enhanced status code found in response');
|
||||
}
|
||||
|
||||
await smtpClient.sendCommand('RSET');
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
@ -205,93 +137,33 @@ tap.test('CCMD-07: Parse response timing and delays', async () => {
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 10000,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
await smtpClient.sendCommand('EHLO testclient.example.com');
|
||||
|
||||
// Measure response times for different commands
|
||||
const timingTests = [
|
||||
'NOOP',
|
||||
'HELP',
|
||||
'MAIL FROM:<sender@example.com>',
|
||||
'RSET'
|
||||
];
|
||||
|
||||
const timings: { command: string; time: number; code: string }[] = [];
|
||||
|
||||
for (const command of timingTests) {
|
||||
const startTime = Date.now();
|
||||
const response = await smtpClient.sendCommand(command);
|
||||
const elapsed = Date.now() - startTime;
|
||||
|
||||
const codeMatch = response.match(/^(\d{3})/);
|
||||
const code = codeMatch ? codeMatch[1] : 'unknown';
|
||||
|
||||
timings.push({ command, time: elapsed, code });
|
||||
}
|
||||
|
||||
// Analyze timings
|
||||
console.log('\nCommand response times:');
|
||||
timings.forEach(t => {
|
||||
console.log(` ${t.command}: ${t.time}ms (${t.code})`);
|
||||
});
|
||||
|
||||
const avgTime = timings.reduce((sum, t) => sum + t.time, 0) / timings.length;
|
||||
console.log(`Average response time: ${avgTime.toFixed(2)}ms`);
|
||||
|
||||
// All commands should respond quickly (under 1 second)
|
||||
timings.forEach(t => {
|
||||
expect(t.time).toBeLessThan(1000);
|
||||
// Measure response time
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: 'Timing Test',
|
||||
text: 'Testing response timing'
|
||||
});
|
||||
|
||||
const startTime = Date.now();
|
||||
const result = await smtpClient.sendMail(email);
|
||||
const elapsed = Date.now() - startTime;
|
||||
|
||||
expect(result.success).toBeTrue();
|
||||
expect(elapsed).toBeGreaterThan(0);
|
||||
expect(elapsed).toBeLessThan(5000); // Should complete within 5 seconds
|
||||
|
||||
console.log(`✅ Response received and parsed in ${elapsed}ms`);
|
||||
console.log('Client handles response timing appropriately');
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CCMD-07: Parse continuation responses', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 10000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
await smtpClient.sendCommand('EHLO testclient.example.com');
|
||||
await smtpClient.sendCommand('MAIL FROM:<sender@example.com>');
|
||||
await smtpClient.sendCommand('RCPT TO:<recipient@example.com>');
|
||||
|
||||
// DATA command returns a continuation response (354)
|
||||
const dataResponse = await smtpClient.sendCommand('DATA');
|
||||
|
||||
// Parse continuation response
|
||||
const contMatch = dataResponse.match(/^(\d{3})[\s-](.*)$/);
|
||||
expect(contMatch).toBeTruthy();
|
||||
|
||||
if (contMatch) {
|
||||
const [, code, text] = contMatch;
|
||||
expect(code).toEqual('354');
|
||||
expect(text).toMatch(/mail input|end with/i);
|
||||
|
||||
console.log(`Continuation response: ${code} ${text}`);
|
||||
}
|
||||
|
||||
// Send message data
|
||||
const messageData = 'Subject: Test\r\n\r\nTest message\r\n.';
|
||||
const finalResponse = await smtpClient.sendCommand(messageData);
|
||||
|
||||
// Parse final response
|
||||
const finalMatch = finalResponse.match(/^(\d{3})/);
|
||||
expect(finalMatch).toBeTruthy();
|
||||
expect(finalMatch![1]).toEqual('250');
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CCMD-07: Parse response text variations', async () => {
|
||||
tap.test('CCMD-07: Parse envelope information', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
@ -300,53 +172,72 @@ tap.test('CCMD-07: Parse response text variations', async () => {
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
const from = 'sender@example.com';
|
||||
const to = ['recipient1@example.com', 'recipient2@example.com'];
|
||||
const cc = ['cc@example.com'];
|
||||
const bcc = ['bcc@example.com'];
|
||||
|
||||
const email = new Email({
|
||||
from,
|
||||
to,
|
||||
cc,
|
||||
bcc,
|
||||
subject: 'Envelope Test',
|
||||
text: 'Testing envelope parsing'
|
||||
});
|
||||
|
||||
// Different servers may have different response text
|
||||
const response = await smtpClient.sendCommand('EHLO testclient.example.com');
|
||||
const result = await smtpClient.sendMail(email);
|
||||
|
||||
// Extract server identification from first line
|
||||
const firstLineMatch = response.match(/^250[\s-](.+?)(?:\r?\n|$)/);
|
||||
expect(result.success).toBeTrue();
|
||||
expect(result.envelope).toBeTruthy();
|
||||
expect(result.envelope.from).toEqual(from);
|
||||
expect(result.envelope.to).toBeArray();
|
||||
|
||||
if (firstLineMatch) {
|
||||
const serverIdent = firstLineMatch[1];
|
||||
console.log(`Server identification: ${serverIdent}`);
|
||||
|
||||
// Check for common patterns
|
||||
const patterns = [
|
||||
{ pattern: /ESMTP/, description: 'Extended SMTP' },
|
||||
{ pattern: /ready|ok|hello/i, description: 'Greeting' },
|
||||
{ pattern: /\d+\.\d+/, description: 'Version number' },
|
||||
{ pattern: /[a-zA-Z0-9.-]+/, description: 'Hostname' }
|
||||
];
|
||||
|
||||
patterns.forEach(p => {
|
||||
if (p.pattern.test(serverIdent)) {
|
||||
console.log(` Found: ${p.description}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
// Envelope should include all recipients (to, cc, bcc)
|
||||
const totalRecipients = to.length + cc.length + bcc.length;
|
||||
expect(result.envelope.to.length).toEqual(totalRecipients);
|
||||
|
||||
console.log(`✅ Envelope parsed with ${result.envelope.to.length} recipients`);
|
||||
console.log('Envelope information correctly extracted from responses');
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
// Test QUIT response variations
|
||||
const quitResponse = await smtpClient.sendCommand('QUIT');
|
||||
const quitMatch = quitResponse.match(/^(\d{3})\s+(.*)$/);
|
||||
tap.test('CCMD-07: Parse connection state responses', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// Test verify() which checks connection state
|
||||
const isConnected = await smtpClient.verify();
|
||||
expect(isConnected).toBeTrue();
|
||||
|
||||
if (quitMatch) {
|
||||
const [, code, text] = quitMatch;
|
||||
expect(code).toEqual('221');
|
||||
|
||||
// Common QUIT response patterns
|
||||
const quitPatterns = ['bye', 'closing', 'goodbye', 'terminating'];
|
||||
const foundPattern = quitPatterns.some(p => text.toLowerCase().includes(p));
|
||||
|
||||
console.log(`QUIT response: ${text} (matches pattern: ${foundPattern})`);
|
||||
}
|
||||
console.log('✅ Connection verified through greeting and EHLO responses');
|
||||
|
||||
// Send email to test active connection
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: 'State Test',
|
||||
text: 'Testing connection state'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
expect(result.success).toBeTrue();
|
||||
|
||||
console.log('✅ Connection state maintained throughout session');
|
||||
console.log('Response parsing handles connection state correctly');
|
||||
|
||||
await smtpClient.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();
|
Reference in New Issue
Block a user