update
This commit is contained in:
@ -5,6 +5,7 @@ import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-clien
|
||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
||||
|
||||
let testServer: ITestServer;
|
||||
let smtpClient: SmtpClient;
|
||||
|
||||
tap.test('setup test SMTP server', async () => {
|
||||
testServer = await startTestServer({
|
||||
@ -16,183 +17,186 @@ tap.test('setup test SMTP server', async () => {
|
||||
expect(testServer.port).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('CCMD-09: Basic NOOP command', 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');
|
||||
|
||||
// Send NOOP command
|
||||
const noopResponse = await smtpClient.sendCommand('NOOP');
|
||||
|
||||
// Verify response
|
||||
expect(noopResponse).toInclude('250');
|
||||
console.log(`NOOP response: ${noopResponse.trim()}`);
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CCMD-09: NOOP during transaction', 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 a transaction
|
||||
await smtpClient.sendCommand('MAIL FROM:<sender@example.com>');
|
||||
await smtpClient.sendCommand('RCPT TO:<recipient@example.com>');
|
||||
|
||||
// Send NOOP - should not affect transaction
|
||||
const noopResponse = await smtpClient.sendCommand('NOOP');
|
||||
expect(noopResponse).toInclude('250');
|
||||
|
||||
// Continue transaction - should still work
|
||||
const dataResponse = await smtpClient.sendCommand('DATA');
|
||||
expect(dataResponse).toInclude('354');
|
||||
|
||||
// Send message
|
||||
const messageResponse = await smtpClient.sendCommand('Subject: Test\r\n\r\nTest message\r\n.');
|
||||
expect(messageResponse).toInclude('250');
|
||||
|
||||
console.log('Transaction completed successfully after NOOP');
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CCMD-09: Multiple NOOP commands', 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');
|
||||
|
||||
// Send multiple NOOPs rapidly
|
||||
const noopCount = 10;
|
||||
const responses: string[] = [];
|
||||
|
||||
console.log(`Sending ${noopCount} NOOP commands...`);
|
||||
|
||||
for (let i = 0; i < noopCount; i++) {
|
||||
const response = await smtpClient.sendCommand('NOOP');
|
||||
responses.push(response);
|
||||
}
|
||||
|
||||
// All should succeed
|
||||
responses.forEach((response, index) => {
|
||||
expect(response).toInclude('250');
|
||||
});
|
||||
|
||||
console.log(`All ${noopCount} NOOP commands succeeded`);
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CCMD-09: NOOP for keep-alive', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
tap.test('CCMD-09: Connection keepalive test', async () => {
|
||||
// NOOP is used internally for keepalive - test that connections remain active
|
||||
smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 10000,
|
||||
debug: true
|
||||
greetingTimeout: 5000,
|
||||
socketTimeout: 10000
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
await smtpClient.sendCommand('EHLO testclient.example.com');
|
||||
// Send an initial email to establish connection
|
||||
const email1 = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Initial connection test',
|
||||
text: 'Testing connection establishment'
|
||||
});
|
||||
|
||||
console.log('Using NOOP for keep-alive over 10 seconds...');
|
||||
await smtpClient.sendMail(email1);
|
||||
console.log('First email sent successfully');
|
||||
|
||||
// Send NOOP every 2 seconds for 10 seconds
|
||||
const keepAliveInterval = 2000;
|
||||
const duration = 10000;
|
||||
const iterations = duration / keepAliveInterval;
|
||||
// Wait 5 seconds (connection should stay alive with internal NOOP)
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
|
||||
// Send another email on the same connection
|
||||
const email2 = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Keepalive test',
|
||||
text: 'Testing connection after delay'
|
||||
});
|
||||
|
||||
await smtpClient.sendMail(email2);
|
||||
console.log('Second email sent successfully after 5 second delay');
|
||||
});
|
||||
|
||||
tap.test('CCMD-09: Multiple emails in sequence', async () => {
|
||||
// Test that client can handle multiple emails without issues
|
||||
// Internal NOOP commands may be used between transactions
|
||||
|
||||
const emails = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
emails.push(new Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i}@example.com`],
|
||||
subject: `Sequential email ${i + 1}`,
|
||||
text: `This is email number ${i + 1}`
|
||||
}));
|
||||
}
|
||||
|
||||
console.log('Sending 5 emails in sequence...');
|
||||
|
||||
for (let i = 0; i < emails.length; i++) {
|
||||
await smtpClient.sendMail(emails[i]);
|
||||
console.log(`Email ${i + 1} sent successfully`);
|
||||
|
||||
// Small delay between emails
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
console.log('All emails sent successfully');
|
||||
});
|
||||
|
||||
tap.test('CCMD-09: Rapid email sending', async () => {
|
||||
// Test rapid email sending without delays
|
||||
// Internal connection management should handle this properly
|
||||
|
||||
const emailCount = 10;
|
||||
const emails = [];
|
||||
|
||||
for (let i = 0; i < emailCount; i++) {
|
||||
emails.push(new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: `Rapid email ${i + 1}`,
|
||||
text: `Rapid fire email number ${i + 1}`
|
||||
}));
|
||||
}
|
||||
|
||||
console.log(`Sending ${emailCount} emails rapidly...`);
|
||||
const startTime = Date.now();
|
||||
|
||||
// Send all emails as fast as possible
|
||||
for (const email of emails) {
|
||||
await smtpClient.sendMail(email);
|
||||
}
|
||||
|
||||
const elapsed = Date.now() - startTime;
|
||||
console.log(`All ${emailCount} emails sent in ${elapsed}ms`);
|
||||
console.log(`Average: ${(elapsed / emailCount).toFixed(2)}ms per email`);
|
||||
});
|
||||
|
||||
tap.test('CCMD-09: Long-lived connection test', async () => {
|
||||
// Test that connection stays alive over extended period
|
||||
// SmtpClient should use internal keepalive mechanisms
|
||||
|
||||
console.log('Testing connection over 10 seconds with periodic emails...');
|
||||
|
||||
const testDuration = 10000;
|
||||
const emailInterval = 2500;
|
||||
const iterations = Math.floor(testDuration / emailInterval);
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
await new Promise(resolve => setTimeout(resolve, keepAliveInterval));
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: `Keepalive test ${i + 1}`,
|
||||
text: `Testing connection keepalive - email ${i + 1}`
|
||||
});
|
||||
|
||||
const startTime = Date.now();
|
||||
const response = await smtpClient.sendCommand('NOOP');
|
||||
await smtpClient.sendMail(email);
|
||||
const elapsed = Date.now() - startTime;
|
||||
|
||||
expect(response).toInclude('250');
|
||||
console.log(`Keep-alive NOOP ${i + 1}: ${elapsed}ms`);
|
||||
console.log(`Email ${i + 1} sent in ${elapsed}ms`);
|
||||
|
||||
if (i < iterations - 1) {
|
||||
await new Promise(resolve => setTimeout(resolve, emailInterval));
|
||||
}
|
||||
}
|
||||
|
||||
// Connection should still be active
|
||||
expect(smtpClient.isConnected()).toBeTruthy();
|
||||
|
||||
await smtpClient.close();
|
||||
|
||||
console.log('Connection remained stable over 10 seconds');
|
||||
});
|
||||
|
||||
tap.test('CCMD-09: NOOP with parameters', 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');
|
||||
|
||||
// RFC 5321 allows NOOP to have parameters (which are ignored)
|
||||
const noopVariants = [
|
||||
'NOOP',
|
||||
'NOOP test',
|
||||
'NOOP hello world',
|
||||
'NOOP 12345',
|
||||
'NOOP check connection'
|
||||
tap.test('CCMD-09: Connection pooling behavior', async () => {
|
||||
// Test connection pooling with different email patterns
|
||||
// Internal NOOP may be used to maintain pool connections
|
||||
|
||||
const testPatterns = [
|
||||
{ count: 3, delay: 0, desc: 'Burst of 3 emails' },
|
||||
{ count: 2, delay: 1000, desc: '2 emails with 1s delay' },
|
||||
{ count: 1, delay: 3000, desc: '1 email after 3s delay' }
|
||||
];
|
||||
|
||||
for (const command of noopVariants) {
|
||||
const response = await smtpClient.sendCommand(command);
|
||||
expect(response).toInclude('250');
|
||||
console.log(`"${command}" -> ${response.trim()}`);
|
||||
|
||||
for (const pattern of testPatterns) {
|
||||
console.log(`\nTesting: ${pattern.desc}`);
|
||||
|
||||
if (pattern.delay > 0) {
|
||||
await new Promise(resolve => setTimeout(resolve, pattern.delay));
|
||||
}
|
||||
|
||||
for (let i = 0; i < pattern.count; i++) {
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: `${pattern.desc} - Email ${i + 1}`,
|
||||
text: 'Testing connection pooling behavior'
|
||||
});
|
||||
|
||||
await smtpClient.sendMail(email);
|
||||
}
|
||||
|
||||
console.log(`Completed: ${pattern.desc}`);
|
||||
}
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CCMD-09: NOOP timing analysis', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: false // Quiet for timing
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
await smtpClient.sendCommand('EHLO testclient.example.com');
|
||||
|
||||
// Measure NOOP response times
|
||||
tap.test('CCMD-09: Email sending performance', async () => {
|
||||
// Measure email sending performance
|
||||
// Connection management (including internal NOOP) affects timing
|
||||
|
||||
const measurements = 20;
|
||||
const times: number[] = [];
|
||||
|
||||
|
||||
console.log(`Measuring performance over ${measurements} emails...`);
|
||||
|
||||
for (let i = 0; i < measurements; i++) {
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: `Performance test ${i + 1}`,
|
||||
text: 'Measuring email sending performance'
|
||||
});
|
||||
|
||||
const startTime = Date.now();
|
||||
await smtpClient.sendCommand('NOOP');
|
||||
await smtpClient.sendMail(email);
|
||||
const elapsed = Date.now() - startTime;
|
||||
times.push(elapsed);
|
||||
}
|
||||
|
||||
|
||||
// Calculate statistics
|
||||
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
|
||||
const minTime = Math.min(...times);
|
||||
@ -201,145 +205,134 @@ tap.test('CCMD-09: NOOP timing analysis', async () => {
|
||||
// Calculate standard deviation
|
||||
const variance = times.reduce((sum, time) => sum + Math.pow(time - avgTime, 2), 0) / times.length;
|
||||
const stdDev = Math.sqrt(variance);
|
||||
|
||||
console.log(`NOOP timing analysis (${measurements} samples):`);
|
||||
|
||||
console.log(`\nPerformance analysis (${measurements} emails):`);
|
||||
console.log(` Average: ${avgTime.toFixed(2)}ms`);
|
||||
console.log(` Min: ${minTime}ms`);
|
||||
console.log(` Max: ${maxTime}ms`);
|
||||
console.log(` Std Dev: ${stdDev.toFixed(2)}ms`);
|
||||
|
||||
// NOOP should be very fast
|
||||
expect(avgTime).toBeLessThan(50);
|
||||
|
||||
// Check for consistency (low standard deviation)
|
||||
expect(stdDev).toBeLessThan(avgTime * 0.5); // Less than 50% of average
|
||||
|
||||
await smtpClient.close();
|
||||
// First email might be slower due to connection establishment
|
||||
const avgWithoutFirst = times.slice(1).reduce((a, b) => a + b, 0) / (times.length - 1);
|
||||
console.log(` Average (excl. first): ${avgWithoutFirst.toFixed(2)}ms`);
|
||||
|
||||
// Performance should be reasonable
|
||||
expect(avgTime).toBeLessThan(200);
|
||||
});
|
||||
|
||||
tap.test('CCMD-09: NOOP during DATA phase', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
tap.test('CCMD-09: Email with NOOP in content', async () => {
|
||||
// Test that NOOP as email content doesn't affect delivery
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Email containing NOOP',
|
||||
text: `This email contains SMTP commands as content:
|
||||
|
||||
NOOP
|
||||
HELO test
|
||||
MAIL FROM:<test@example.com>
|
||||
|
||||
These should be treated as plain text, not commands.
|
||||
The word NOOP appears multiple times in this email.
|
||||
|
||||
NOOP is used internally by SMTP for keepalive.`
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
await smtpClient.sendCommand('EHLO testclient.example.com');
|
||||
|
||||
// Setup transaction
|
||||
await smtpClient.sendCommand('MAIL FROM:<sender@example.com>');
|
||||
await smtpClient.sendCommand('RCPT TO:<recipient@example.com>');
|
||||
|
||||
// Enter DATA phase
|
||||
const dataResponse = await smtpClient.sendCommand('DATA');
|
||||
expect(dataResponse).toInclude('354');
|
||||
|
||||
// During DATA phase, NOOP will be treated as message content
|
||||
await smtpClient.sendCommand('Subject: Test with NOOP');
|
||||
await smtpClient.sendCommand('');
|
||||
await smtpClient.sendCommand('This message contains the word NOOP');
|
||||
await smtpClient.sendCommand('NOOP'); // This is message content, not a command
|
||||
await smtpClient.sendCommand('End of message');
|
||||
await smtpClient.sendMail(email);
|
||||
console.log('Email with NOOP content sent successfully');
|
||||
|
||||
// End DATA phase
|
||||
const endResponse = await smtpClient.sendCommand('.');
|
||||
expect(endResponse).toInclude('250');
|
||||
|
||||
// Now NOOP should work as a command again
|
||||
const noopResponse = await smtpClient.sendCommand('NOOP');
|
||||
expect(noopResponse).toInclude('250');
|
||||
|
||||
console.log('NOOP works correctly after DATA phase');
|
||||
|
||||
await smtpClient.close();
|
||||
// Send another email to verify connection still works
|
||||
const email2 = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Follow-up email',
|
||||
text: 'Verifying connection still works after NOOP content'
|
||||
});
|
||||
|
||||
await smtpClient.sendMail(email2);
|
||||
console.log('Follow-up email sent successfully');
|
||||
});
|
||||
|
||||
tap.test('CCMD-09: NOOP in pipelined commands', 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 NOOP with other commands
|
||||
console.log('Pipelining NOOP with other commands...');
|
||||
tap.test('CCMD-09: Concurrent email sending', async () => {
|
||||
// Test concurrent email sending
|
||||
// Connection pooling and internal management should handle this
|
||||
|
||||
const pipelinedCommands = [
|
||||
smtpClient.sendCommand('NOOP'),
|
||||
smtpClient.sendCommand('MAIL FROM:<sender@example.com>'),
|
||||
smtpClient.sendCommand('NOOP'),
|
||||
smtpClient.sendCommand('RCPT TO:<recipient@example.com>'),
|
||||
smtpClient.sendCommand('NOOP'),
|
||||
smtpClient.sendCommand('RSET'),
|
||||
smtpClient.sendCommand('NOOP')
|
||||
];
|
||||
|
||||
const responses = await Promise.all(pipelinedCommands);
|
||||
|
||||
// All commands should succeed
|
||||
responses.forEach((response, index) => {
|
||||
expect(response).toInclude('250');
|
||||
});
|
||||
|
||||
console.log('All pipelined commands including NOOPs succeeded');
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CCMD-09: NOOP error scenarios', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
const concurrentCount = 5;
|
||||
const emails = [];
|
||||
|
||||
// Try NOOP before EHLO/HELO (some servers might reject)
|
||||
const earlyNoop = await smtpClient.sendCommand('NOOP');
|
||||
console.log(`NOOP before EHLO: ${earlyNoop.trim()}`);
|
||||
for (let i = 0; i < concurrentCount; i++) {
|
||||
emails.push(new Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i}@example.com`],
|
||||
subject: `Concurrent email ${i + 1}`,
|
||||
text: `Testing concurrent email sending - message ${i + 1}`
|
||||
}));
|
||||
}
|
||||
|
||||
// Most servers allow it, but check response
|
||||
expect(earlyNoop).toMatch(/[25]\d\d/);
|
||||
|
||||
// Now do proper handshake
|
||||
await smtpClient.sendCommand('EHLO testclient.example.com');
|
||||
|
||||
// Test malformed NOOP (though it should be accepted)
|
||||
const malformedTests = [
|
||||
'NOOP\t\ttabs',
|
||||
'NOOP multiple spaces',
|
||||
'noop lowercase',
|
||||
'NoOp MixedCase'
|
||||
];
|
||||
|
||||
for (const command of malformedTests) {
|
||||
try {
|
||||
const response = await smtpClient.sendCommand(command);
|
||||
console.log(`"${command}" -> ${response.trim()}`);
|
||||
// Most servers are lenient
|
||||
} catch (error) {
|
||||
console.log(`"${command}" -> Error: ${error.message}`);
|
||||
console.log(`Sending ${concurrentCount} emails concurrently...`);
|
||||
const startTime = Date.now();
|
||||
|
||||
// Send all emails concurrently
|
||||
try {
|
||||
await Promise.all(emails.map(email => smtpClient.sendMail(email)));
|
||||
const elapsed = Date.now() - startTime;
|
||||
console.log(`All ${concurrentCount} emails sent concurrently in ${elapsed}ms`);
|
||||
} catch (error) {
|
||||
// Concurrent sending might not be supported - that's OK
|
||||
console.log('Concurrent sending not supported, falling back to sequential');
|
||||
for (const email of emails) {
|
||||
await smtpClient.sendMail(email);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await smtpClient.close();
|
||||
tap.test('CCMD-09: Connection recovery test', async () => {
|
||||
// Test connection recovery and error handling
|
||||
// SmtpClient should handle connection issues gracefully
|
||||
|
||||
// Create a new client with shorter timeouts for testing
|
||||
const testClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 3000,
|
||||
socketTimeout: 3000
|
||||
});
|
||||
|
||||
// Send initial email
|
||||
const email1 = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Connection test 1',
|
||||
text: 'Testing initial connection'
|
||||
});
|
||||
|
||||
await testClient.sendMail(email1);
|
||||
console.log('Initial email sent');
|
||||
|
||||
// Simulate long delay that might timeout connection
|
||||
console.log('Waiting 5 seconds to test connection recovery...');
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
|
||||
// Try to send another email - client should recover if needed
|
||||
const email2 = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Connection test 2',
|
||||
text: 'Testing connection recovery'
|
||||
});
|
||||
|
||||
try {
|
||||
await testClient.sendMail(email2);
|
||||
console.log('Email sent successfully after delay - connection recovered');
|
||||
} catch (error) {
|
||||
console.log('Connection recovery failed (this might be expected):', error.message);
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('cleanup test SMTP server', async () => {
|
||||
if (testServer) {
|
||||
await testServer.stop();
|
||||
await stopTestServer(testServer);
|
||||
}
|
||||
});
|
||||
|
||||
|
Reference in New Issue
Block a user