This commit is contained in:
2025-05-24 16:19:19 +00:00
parent 3d669ed9dd
commit 4e4c7df558
50 changed files with 17232 additions and 340 deletions

View File

@ -0,0 +1,136 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
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';
let testServer: ITestServer;
let smtpClient: SmtpClient;
tap.test('setup - start SMTP server for basic connection test', async () => {
testServer = await startTestServer({
port: 2525,
tlsEnabled: false,
authRequired: false
});
expect(testServer.port).toEqual(2525);
});
tap.test('CCM-01: Basic TCP Connection - should connect to SMTP server', async () => {
const startTime = Date.now();
try {
// Create SMTP client
smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
// Verify connection
const isConnected = await smtpClient.verify();
expect(isConnected).toBeTrue();
const duration = Date.now() - startTime;
console.log(`✅ Basic TCP connection established in ${duration}ms`);
} catch (error) {
const duration = Date.now() - startTime;
console.error(`❌ Basic TCP connection failed after ${duration}ms:`, error);
throw error;
}
});
tap.test('CCM-01: Basic TCP Connection - should report connection status', async () => {
expect(smtpClient.isConnected()).toBeTrue();
const poolStatus = smtpClient.getPoolStatus();
console.log('📊 Connection pool status:', poolStatus);
// For non-pooled connection, should have 1 connection
expect(poolStatus.total).toBeGreaterThanOrEqual(1);
expect(poolStatus.active).toBeGreaterThanOrEqual(0);
});
tap.test('CCM-01: Basic TCP Connection - should handle multiple connect/disconnect cycles', async () => {
// Close existing connection
await smtpClient.close();
expect(smtpClient.isConnected()).toBeFalse();
// Create new client and test reconnection
for (let i = 0; i < 3; i++) {
const cycleClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000
});
const isConnected = await cycleClient.verify();
expect(isConnected).toBeTrue();
await cycleClient.close();
expect(cycleClient.isConnected()).toBeFalse();
console.log(`✅ Connection cycle ${i + 1} completed`);
}
});
tap.test('CCM-01: Basic TCP Connection - should fail with invalid host', async () => {
let errorThrown = false;
try {
const invalidClient = createSmtpClient({
host: 'invalid.host.that.does.not.exist',
port: 2525,
secure: false,
connectionTimeout: 3000
});
await invalidClient.verify();
} catch (error) {
errorThrown = true;
expect(error).toBeInstanceOf(Error);
console.log('✅ Correctly failed to connect to invalid host');
}
expect(errorThrown).toBeTrue();
});
tap.test('CCM-01: Basic TCP Connection - should timeout on unresponsive port', async () => {
let errorThrown = false;
const startTime = Date.now();
try {
const timeoutClient = createSmtpClient({
host: testServer.hostname,
port: 9999, // Port that's not listening
secure: false,
connectionTimeout: 2000
});
await timeoutClient.verify();
} catch (error) {
errorThrown = true;
const duration = Date.now() - startTime;
expect(error).toBeInstanceOf(Error);
expect(duration).toBeLessThan(3000); // Should timeout within 3 seconds
console.log(`✅ Connection timeout working correctly (${duration}ms)`);
}
expect(errorThrown).toBeTrue();
});
tap.test('cleanup - close SMTP client', async () => {
if (smtpClient && smtpClient.isConnected()) {
await smtpClient.close();
}
});
tap.test('cleanup - stop SMTP server', async () => {
await stopTestServer(testServer);
});
tap.start();

View File

@ -0,0 +1,162 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
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: ITestServer;
let smtpClient: SmtpClient;
tap.test('setup - start SMTP server with TLS', async () => {
testServer = await startTestServer({
port: 2526,
tlsEnabled: true,
authRequired: false
});
expect(testServer.port).toEqual(2526);
expect(testServer.config.tlsEnabled).toBeTrue();
});
tap.test('CCM-02: TLS Connection - should establish secure connection', async () => {
const startTime = Date.now();
try {
// Create SMTP client with TLS
smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: true,
connectionTimeout: 10000,
tls: {
rejectUnauthorized: false // For self-signed test certificates
},
debug: true
});
// Verify secure connection
const isConnected = await smtpClient.verify();
expect(isConnected).toBeTrue();
const duration = Date.now() - startTime;
console.log(`✅ TLS connection established in ${duration}ms`);
} catch (error) {
const duration = Date.now() - startTime;
console.error(`❌ TLS connection failed after ${duration}ms:`, error);
throw error;
}
});
tap.test('CCM-02: TLS Connection - should send email over secure connection', async () => {
const email = new Email({
from: 'test@example.com',
to: 'recipient@example.com',
subject: 'TLS Connection Test',
text: 'This email was sent over a secure TLS connection',
html: '<p>This email was sent over a <strong>secure TLS connection</strong></p>'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
expect(result.acceptedRecipients).toContain('recipient@example.com');
expect(result.rejectedRecipients).toBeArray();
expect(result.rejectedRecipients.length).toEqual(0);
console.log('✅ Email sent successfully over TLS');
console.log('📧 Message ID:', result.messageId);
});
tap.test('CCM-02: TLS Connection - should validate certificate options', async () => {
let errorThrown = false;
try {
// Create client with strict certificate validation
const strictClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: true,
connectionTimeout: 5000,
tls: {
rejectUnauthorized: true, // Strict validation
servername: testServer.hostname
}
});
await strictClient.verify();
await strictClient.close();
} catch (error) {
errorThrown = true;
// Expected to fail with self-signed certificate
expect(error).toBeInstanceOf(Error);
console.log('✅ Certificate validation working correctly');
}
// For self-signed certs, strict validation should fail
expect(errorThrown).toBeTrue();
});
tap.test('CCM-02: TLS Connection - should support custom TLS options', async () => {
const tlsClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: true,
connectionTimeout: 10000,
tls: {
rejectUnauthorized: false,
minVersion: 'TLSv1.2',
maxVersion: 'TLSv1.3'
}
});
const isConnected = await tlsClient.verify();
expect(isConnected).toBeTrue();
await tlsClient.close();
console.log('✅ Custom TLS options accepted');
});
tap.test('CCM-02: TLS Connection - should handle TLS handshake errors', async () => {
// Start a non-TLS server to test handshake failure
const nonTlsServer = await startTestServer({
port: 2527,
tlsEnabled: false
});
let errorThrown = false;
try {
const failClient = createSmtpClient({
host: nonTlsServer.hostname,
port: nonTlsServer.port,
secure: true, // Try TLS on non-TLS server
connectionTimeout: 5000,
tls: {
rejectUnauthorized: false
}
});
await failClient.verify();
} catch (error) {
errorThrown = true;
expect(error).toBeInstanceOf(Error);
console.log('✅ TLS handshake error handled correctly');
}
expect(errorThrown).toBeTrue();
await stopTestServer(nonTlsServer);
});
tap.test('cleanup - close SMTP client', async () => {
if (smtpClient && smtpClient.isConnected()) {
await smtpClient.close();
}
});
tap.test('cleanup - stop SMTP server', async () => {
await stopTestServer(testServer);
});
tap.start();

View File

@ -0,0 +1,200 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
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: ITestServer;
let smtpClient: SmtpClient;
tap.test('setup - start SMTP server with STARTTLS support', async () => {
testServer = await startTestServer({
port: 2528,
tlsEnabled: true, // Enables STARTTLS capability
authRequired: false
});
expect(testServer.port).toEqual(2528);
});
tap.test('CCM-03: STARTTLS Upgrade - should upgrade plain connection to TLS', async () => {
const startTime = Date.now();
try {
// Create SMTP client starting with plain connection
smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false, // Start with plain connection
connectionTimeout: 10000,
tls: {
rejectUnauthorized: false // For self-signed test certificates
},
debug: true
});
// The client should automatically upgrade to TLS via STARTTLS
const isConnected = await smtpClient.verify();
expect(isConnected).toBeTrue();
const duration = Date.now() - startTime;
console.log(`✅ STARTTLS upgrade completed in ${duration}ms`);
} catch (error) {
const duration = Date.now() - startTime;
console.error(`❌ STARTTLS upgrade failed after ${duration}ms:`, error);
throw error;
}
});
tap.test('CCM-03: STARTTLS Upgrade - should send email after upgrade', async () => {
const email = new Email({
from: 'test@example.com',
to: 'recipient@example.com',
subject: 'STARTTLS Upgrade Test',
text: 'This email was sent after STARTTLS upgrade',
html: '<p>This email was sent after <strong>STARTTLS upgrade</strong></p>'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
expect(result.acceptedRecipients).toContain('recipient@example.com');
expect(result.rejectedRecipients.length).toEqual(0);
console.log('✅ Email sent successfully after STARTTLS upgrade');
console.log('📧 Message ID:', result.messageId);
});
tap.test('CCM-03: STARTTLS Upgrade - should handle servers without STARTTLS', async () => {
// Start a server without TLS support
const plainServer = await startTestServer({
port: 2529,
tlsEnabled: false // No STARTTLS support
});
try {
const plainClient = createSmtpClient({
host: plainServer.hostname,
port: plainServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
// Should still connect but without TLS
const isConnected = await plainClient.verify();
expect(isConnected).toBeTrue();
// Send test email over plain connection
const email = new Email({
from: 'test@example.com',
to: 'recipient@example.com',
subject: 'Plain Connection Test',
text: 'This email was sent over plain connection'
});
const result = await plainClient.sendMail(email);
expect(result.success).toBeTrue();
await plainClient.close();
console.log('✅ Successfully handled server without STARTTLS');
} finally {
await stopTestServer(plainServer);
}
});
tap.test('CCM-03: STARTTLS Upgrade - should respect TLS options during upgrade', async () => {
const customTlsClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false, // Start plain
connectionTimeout: 10000,
tls: {
rejectUnauthorized: false,
minVersion: 'TLSv1.2',
ciphers: 'HIGH:!aNULL:!MD5'
}
});
const isConnected = await customTlsClient.verify();
expect(isConnected).toBeTrue();
await customTlsClient.close();
console.log('✅ Custom TLS options applied during STARTTLS upgrade');
});
tap.test('CCM-03: STARTTLS Upgrade - should handle upgrade failures gracefully', async () => {
// Create a mock scenario where STARTTLS might fail
// This would typically happen with certificate issues or protocol mismatches
let errorCaught = false;
try {
const strictTlsClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
tls: {
rejectUnauthorized: true, // Strict validation with self-signed cert
servername: 'wrong.hostname.com' // Wrong hostname
}
});
await strictTlsClient.verify();
await strictTlsClient.close();
} catch (error) {
errorCaught = true;
expect(error).toBeInstanceOf(Error);
console.log('✅ STARTTLS upgrade failure handled gracefully');
}
// Should fail due to certificate validation
expect(errorCaught).toBeTrue();
});
tap.test('CCM-03: STARTTLS Upgrade - should maintain connection state after upgrade', async () => {
const stateClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 10000,
tls: {
rejectUnauthorized: false
}
});
// Connect and verify
await stateClient.verify();
expect(stateClient.isConnected()).toBeTrue();
// Send multiple emails to verify connection remains stable
for (let i = 0; i < 3; i++) {
const email = new Email({
from: 'test@example.com',
to: 'recipient@example.com',
subject: `STARTTLS State Test ${i + 1}`,
text: `Message ${i + 1} after STARTTLS upgrade`
});
const result = await stateClient.sendMail(email);
expect(result.success).toBeTrue();
}
await stateClient.close();
console.log('✅ Connection state maintained after STARTTLS upgrade');
});
tap.test('cleanup - close SMTP client', async () => {
if (smtpClient && smtpClient.isConnected()) {
await smtpClient.close();
}
});
tap.test('cleanup - stop SMTP server', async () => {
await stopTestServer(testServer);
});
tap.start();

View File

@ -0,0 +1,238 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
import { createPooledSmtpClient } 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: ITestServer;
let pooledClient: SmtpClient;
tap.test('setup - start SMTP server for pooling test', async () => {
testServer = await startTestServer({
port: 2530,
tlsEnabled: false,
authRequired: false,
maxConnections: 10
});
expect(testServer.port).toEqual(2530);
});
tap.test('CCM-04: Connection Pooling - should create pooled client', async () => {
const startTime = Date.now();
try {
// Create pooled SMTP client
pooledClient = createPooledSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
maxConnections: 5,
maxMessages: 100,
connectionTimeout: 5000,
debug: true
});
// Verify connection pool is working
const isConnected = await pooledClient.verify();
expect(isConnected).toBeTrue();
const poolStatus = pooledClient.getPoolStatus();
console.log('📊 Initial pool status:', poolStatus);
expect(poolStatus.total).toBeGreaterThanOrEqual(0);
const duration = Date.now() - startTime;
console.log(`✅ Connection pool created in ${duration}ms`);
} catch (error) {
const duration = Date.now() - startTime;
console.error(`❌ Connection pool creation failed after ${duration}ms:`, error);
throw error;
}
});
tap.test('CCM-04: Connection Pooling - should handle concurrent connections', async () => {
// Send multiple emails concurrently
const emailPromises = [];
const concurrentCount = 5;
for (let i = 0; i < concurrentCount; i++) {
const email = new Email({
from: 'test@example.com',
to: `recipient${i}@example.com`,
subject: `Concurrent Email ${i}`,
text: `This is concurrent email number ${i}`
});
emailPromises.push(pooledClient.sendMail(email));
}
// Wait for all emails to be sent
const results = await Promise.all(emailPromises);
// Check all were successful
results.forEach((result, index) => {
expect(result.success).toBeTrue();
expect(result.acceptedRecipients).toContain(`recipient${index}@example.com`);
});
// Check pool status after concurrent sends
const poolStatus = pooledClient.getPoolStatus();
console.log('📊 Pool status after concurrent sends:', poolStatus);
expect(poolStatus.total).toBeGreaterThanOrEqual(1);
expect(poolStatus.total).toBeLessThanOrEqual(5); // Should not exceed max
console.log(`✅ Successfully sent ${concurrentCount} concurrent emails`);
});
tap.test('CCM-04: Connection Pooling - should reuse connections', async () => {
// Get initial pool status
const initialStatus = pooledClient.getPoolStatus();
console.log('📊 Initial status:', initialStatus);
// Send emails sequentially to test connection reuse
const emailCount = 10;
const connectionCounts = [];
for (let i = 0; i < emailCount; i++) {
const email = new Email({
from: 'test@example.com',
to: 'recipient@example.com',
subject: `Sequential Email ${i}`,
text: `Testing connection reuse - email ${i}`
});
await pooledClient.sendMail(email);
const status = pooledClient.getPoolStatus();
connectionCounts.push(status.total);
}
// Check that connections were reused (total shouldn't grow linearly)
const maxConnections = Math.max(...connectionCounts);
expect(maxConnections).toBeLessThan(emailCount); // Should reuse connections
console.log(`✅ Sent ${emailCount} emails using max ${maxConnections} connections`);
console.log('📊 Connection counts:', connectionCounts);
});
tap.test('CCM-04: Connection Pooling - should respect max connections limit', async () => {
// Create a client with small pool
const limitedClient = createPooledSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
maxConnections: 2, // Very small pool
connectionTimeout: 5000
});
// Send many concurrent emails
const emailPromises = [];
for (let i = 0; i < 10; i++) {
const email = new Email({
from: 'test@example.com',
to: `test${i}@example.com`,
subject: `Pool Limit Test ${i}`,
text: 'Testing pool limits'
});
emailPromises.push(limitedClient.sendMail(email));
}
// Monitor pool during sending
const checkInterval = setInterval(() => {
const status = limitedClient.getPoolStatus();
console.log('📊 Pool status during load:', status);
expect(status.total).toBeLessThanOrEqual(2); // Should never exceed max
}, 100);
await Promise.all(emailPromises);
clearInterval(checkInterval);
await limitedClient.close();
console.log('✅ Connection pool respected max connections limit');
});
tap.test('CCM-04: Connection Pooling - should handle connection failures in pool', async () => {
// Create a new pooled client
const resilientClient = createPooledSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
maxConnections: 3,
connectionTimeout: 5000
});
// Send some emails successfully
for (let i = 0; i < 3; i++) {
const email = new Email({
from: 'test@example.com',
to: 'recipient@example.com',
subject: `Pre-failure Email ${i}`,
text: 'Before simulated failure'
});
const result = await resilientClient.sendMail(email);
expect(result.success).toBeTrue();
}
// Pool should recover and continue working
const poolStatus = resilientClient.getPoolStatus();
console.log('📊 Pool status after recovery test:', poolStatus);
expect(poolStatus.total).toBeGreaterThanOrEqual(1);
await resilientClient.close();
console.log('✅ Connection pool handled failures gracefully');
});
tap.test('CCM-04: Connection Pooling - should clean up idle connections', async () => {
// Create client with specific idle settings
const idleClient = createPooledSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
maxConnections: 5,
connectionTimeout: 5000
});
// Send burst of emails
const promises = [];
for (let i = 0; i < 5; i++) {
const email = new Email({
from: 'test@example.com',
to: 'recipient@example.com',
subject: `Idle Test ${i}`,
text: 'Testing idle cleanup'
});
promises.push(idleClient.sendMail(email));
}
await Promise.all(promises);
const activeStatus = idleClient.getPoolStatus();
console.log('📊 Pool status after burst:', activeStatus);
// Wait for connections to become idle
await new Promise(resolve => setTimeout(resolve, 2000));
const idleStatus = idleClient.getPoolStatus();
console.log('📊 Pool status after idle period:', idleStatus);
await idleClient.close();
console.log('✅ Idle connection management working');
});
tap.test('cleanup - close pooled client', async () => {
if (pooledClient && pooledClient.isConnected()) {
await pooledClient.close();
// Verify pool is cleaned up
const finalStatus = pooledClient.getPoolStatus();
console.log('📊 Final pool status:', finalStatus);
}
});
tap.test('cleanup - stop SMTP server', async () => {
await stopTestServer(testServer);
});
tap.start();

View File

@ -0,0 +1,290 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
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: ITestServer;
let smtpClient: SmtpClient;
tap.test('setup - start SMTP server for connection reuse test', async () => {
testServer = await startTestServer({
port: 2531,
tlsEnabled: false,
authRequired: false
});
expect(testServer.port).toEqual(2531);
});
tap.test('CCM-05: Connection Reuse - should reuse single connection for multiple emails', async () => {
const startTime = Date.now();
smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
// Verify initial connection
await smtpClient.verify();
expect(smtpClient.isConnected()).toBeTrue();
// Send multiple emails on same connection
const emailCount = 5;
const results = [];
for (let i = 0; i < emailCount; i++) {
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: `Connection Reuse Test ${i + 1}`,
text: `This is email ${i + 1} using the same connection`
});
const result = await smtpClient.sendMail(email);
results.push(result);
// Connection should remain open
expect(smtpClient.isConnected()).toBeTrue();
}
// All emails should succeed
results.forEach((result, index) => {
expect(result.success).toBeTrue();
console.log(`✅ Email ${index + 1} sent successfully`);
});
const duration = Date.now() - startTime;
console.log(`✅ Sent ${emailCount} emails on single connection in ${duration}ms`);
});
tap.test('CCM-05: Connection Reuse - should track message count per connection', async () => {
// Create a new client with message limit
const limitedClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
maxMessages: 3, // Limit messages per connection
connectionTimeout: 5000
});
// Send emails up to and beyond the limit
for (let i = 0; i < 5; i++) {
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: `Message Limit Test ${i + 1}`,
text: `Testing message limits`
});
const result = await limitedClient.sendMail(email);
expect(result.success).toBeTrue();
// After 3 messages, connection should be refreshed
if (i === 2) {
console.log('✅ Connection should refresh after message limit');
}
}
await limitedClient.close();
});
tap.test('CCM-05: Connection Reuse - should handle connection state changes', async () => {
// Monitor connection state during reuse
let connectionEvents = 0;
const eventClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000
});
eventClient.on('connect', () => connectionEvents++);
// First email
const email1 = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'First Email',
text: 'Testing connection events'
});
await eventClient.sendMail(email1);
const firstConnectCount = connectionEvents;
// Second email (should reuse connection)
const email2 = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Second Email',
text: 'Should reuse connection'
});
await eventClient.sendMail(email2);
// Should not have created new connection
expect(connectionEvents).toEqual(firstConnectCount);
await eventClient.close();
console.log(`✅ Connection reused (${connectionEvents} total connections)`);
});
tap.test('CCM-05: Connection Reuse - should handle idle connection timeout', async () => {
const idleClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
socketTimeout: 3000 // Short timeout for testing
});
// Send first email
const email1 = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Pre-idle Email',
text: 'Before idle period'
});
await idleClient.sendMail(email1);
expect(idleClient.isConnected()).toBeTrue();
// Wait for potential idle timeout
console.log('⏳ Testing idle connection behavior...');
await new Promise(resolve => setTimeout(resolve, 4000));
// Send another email
const email2 = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Post-idle Email',
text: 'After idle period'
});
// Should handle reconnection if needed
const result = await idleClient.sendMail(email2);
expect(result.success).toBeTrue();
await idleClient.close();
console.log('✅ Idle connection handling working correctly');
});
tap.test('CCM-05: Connection Reuse - should optimize performance with reuse', async () => {
// Compare performance with and without connection reuse
// Test 1: Multiple connections (no reuse)
const noReuseStart = Date.now();
for (let i = 0; i < 3; i++) {
const tempClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000
});
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: `No Reuse ${i}`,
text: 'Testing without reuse'
});
await tempClient.sendMail(email);
await tempClient.close();
}
const noReuseDuration = Date.now() - noReuseStart;
// Test 2: Single connection (with reuse)
const reuseStart = Date.now();
const reuseClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000
});
for (let i = 0; i < 3; i++) {
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: `With Reuse ${i}`,
text: 'Testing with reuse'
});
await reuseClient.sendMail(email);
}
await reuseClient.close();
const reuseDuration = Date.now() - reuseStart;
console.log(`📊 Performance comparison:`);
console.log(` Without reuse: ${noReuseDuration}ms`);
console.log(` With reuse: ${reuseDuration}ms`);
console.log(` Improvement: ${Math.round((1 - reuseDuration/noReuseDuration) * 100)}%`);
// Reuse should be faster
expect(reuseDuration).toBeLessThan(noReuseDuration);
});
tap.test('CCM-05: Connection Reuse - should handle errors without breaking reuse', async () => {
const resilientClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000
});
// Send valid email
const validEmail = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Valid Email',
text: 'This should work'
});
const result1 = await resilientClient.sendMail(validEmail);
expect(result1.success).toBeTrue();
// Try to send invalid email
try {
const invalidEmail = new Email({
from: 'invalid sender format',
to: 'recipient@example.com',
subject: 'Invalid Email',
text: 'This should fail'
});
await resilientClient.sendMail(invalidEmail);
} catch (error) {
console.log('✅ Invalid email rejected as expected');
}
// Connection should still be usable
const validEmail2 = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Valid Email After Error',
text: 'Connection should still work'
});
const result2 = await resilientClient.sendMail(validEmail2);
expect(result2.success).toBeTrue();
await resilientClient.close();
console.log('✅ Connection reuse survived error condition');
});
tap.test('cleanup - close SMTP client', async () => {
if (smtpClient && smtpClient.isConnected()) {
await smtpClient.close();
}
});
tap.test('cleanup - stop SMTP server', async () => {
await stopTestServer(testServer);
});
tap.start();

View File

@ -0,0 +1,285 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
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';
import * as net from 'net';
let testServer: ITestServer;
tap.test('setup - start SMTP server for timeout tests', async () => {
testServer = await startTestServer({
port: 2532,
tlsEnabled: false,
authRequired: false
});
expect(testServer.port).toEqual(2532);
});
tap.test('CCM-06: Connection Timeout - should timeout on unresponsive server', async () => {
const startTime = Date.now();
let timeoutError = false;
try {
const timeoutClient = createSmtpClient({
host: testServer.hostname,
port: 9999, // Non-existent port
secure: false,
connectionTimeout: 2000, // 2 second timeout
debug: true
});
await timeoutClient.verify();
} catch (error: any) {
timeoutError = true;
const duration = Date.now() - startTime;
expect(error).toBeInstanceOf(Error);
expect(duration).toBeLessThan(3000); // Should timeout within 3s
expect(duration).toBeGreaterThan(1500); // But not too early
console.log(`✅ Connection timeout after ${duration}ms`);
console.log(` Error: ${error.message}`);
}
expect(timeoutError).toBeTrue();
});
tap.test('CCM-06: Connection Timeout - should handle slow server response', async () => {
// Create a mock slow server
const slowServer = net.createServer((socket) => {
// Accept connection but delay response
setTimeout(() => {
socket.write('220 Slow server ready\r\n');
}, 3000); // 3 second delay
});
await new Promise<void>((resolve) => {
slowServer.listen(2533, () => resolve());
});
const startTime = Date.now();
let timeoutOccurred = false;
try {
const slowClient = createSmtpClient({
host: 'localhost',
port: 2533,
secure: false,
connectionTimeout: 1000, // 1 second timeout
debug: true
});
await slowClient.verify();
} catch (error: any) {
timeoutOccurred = true;
const duration = Date.now() - startTime;
expect(duration).toBeLessThan(2000);
console.log(`✅ Slow server timeout after ${duration}ms`);
}
expect(timeoutOccurred).toBeTrue();
slowServer.close();
await new Promise(resolve => setTimeout(resolve, 100));
});
tap.test('CCM-06: Connection Timeout - should respect socket timeout during data transfer', async () => {
const socketTimeoutClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
socketTimeout: 10000, // 10 second socket timeout
debug: true
});
await socketTimeoutClient.verify();
// Send a normal email
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Socket Timeout Test',
text: 'Testing socket timeout configuration'
});
const result = await socketTimeoutClient.sendMail(email);
expect(result.success).toBeTrue();
await socketTimeoutClient.close();
console.log('✅ Socket timeout configuration applied');
});
tap.test('CCM-06: Connection Timeout - should handle timeout during TLS handshake', async () => {
// Create a server that accepts connections but doesn't complete TLS
const badTlsServer = net.createServer((socket) => {
// Accept connection but don't respond to TLS
socket.on('data', () => {
// Do nothing - simulate hung TLS handshake
});
});
await new Promise<void>((resolve) => {
badTlsServer.listen(2534, () => resolve());
});
let tlsTimeoutError = false;
const startTime = Date.now();
try {
const tlsTimeoutClient = createSmtpClient({
host: 'localhost',
port: 2534,
secure: true, // Try TLS
connectionTimeout: 2000,
tls: {
rejectUnauthorized: false
}
});
await tlsTimeoutClient.verify();
} catch (error: any) {
tlsTimeoutError = true;
const duration = Date.now() - startTime;
expect(duration).toBeLessThan(3000);
console.log(`✅ TLS handshake timeout after ${duration}ms`);
}
expect(tlsTimeoutError).toBeTrue();
badTlsServer.close();
await new Promise(resolve => setTimeout(resolve, 100));
});
tap.test('CCM-06: Connection Timeout - should not timeout on successful quick connection', async () => {
const quickClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 30000, // Very long timeout
debug: true
});
const startTime = Date.now();
const isConnected = await quickClient.verify();
const duration = Date.now() - startTime;
expect(isConnected).toBeTrue();
expect(duration).toBeLessThan(5000); // Should connect quickly
await quickClient.close();
console.log(`✅ Quick connection established in ${duration}ms`);
});
tap.test('CCM-06: Connection Timeout - should handle timeout during authentication', async () => {
// Start auth server
const authServer = await startTestServer({
port: 2535,
authRequired: true
});
// Create mock auth that delays
const authTimeoutClient = createSmtpClient({
host: authServer.hostname,
port: authServer.port,
secure: false,
connectionTimeout: 5000,
socketTimeout: 1000, // Very short socket timeout
auth: {
user: 'testuser',
pass: 'testpass'
}
});
try {
await authTimeoutClient.verify();
// If this succeeds, auth was fast enough
await authTimeoutClient.close();
console.log('✅ Authentication completed within timeout');
} catch (error) {
console.log('✅ Authentication timeout handled');
}
await stopTestServer(authServer);
});
tap.test('CCM-06: Connection Timeout - should apply different timeouts for different operations', async () => {
const multiTimeoutClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000, // Connection establishment
socketTimeout: 30000, // Data operations
debug: true
});
// Connection should be quick
const connectStart = Date.now();
await multiTimeoutClient.verify();
const connectDuration = Date.now() - connectStart;
expect(connectDuration).toBeLessThan(5000);
// Send email with potentially longer operation
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Multi-timeout Test',
text: 'Testing different timeout values',
attachments: [{
filename: 'test.txt',
content: Buffer.from('Test content'),
contentType: 'text/plain'
}]
});
const sendStart = Date.now();
const result = await multiTimeoutClient.sendMail(email);
const sendDuration = Date.now() - sendStart;
expect(result.success).toBeTrue();
console.log(`✅ Different timeouts applied: connect=${connectDuration}ms, send=${sendDuration}ms`);
await multiTimeoutClient.close();
});
tap.test('CCM-06: Connection Timeout - should retry after timeout with pooled connections', async () => {
const retryClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 2,
connectionTimeout: 5000,
debug: true
});
// First connection should succeed
const email1 = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Pre-timeout Email',
text: 'Before any timeout'
});
const result1 = await retryClient.sendMail(email1);
expect(result1.success).toBeTrue();
// Pool should handle connection management
const poolStatus = retryClient.getPoolStatus();
console.log('📊 Pool status:', poolStatus);
await retryClient.close();
console.log('✅ Connection pool handles timeouts gracefully');
});
tap.test('cleanup - stop SMTP server', async () => {
await stopTestServer(testServer);
});
tap.start();

View File

@ -0,0 +1,308 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
import { createSmtpClient, createPooledSmtpClient } 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';
import * as net from 'net';
let testServer: ITestServer;
tap.test('setup - start SMTP server for reconnection tests', async () => {
testServer = await startTestServer({
port: 2533,
tlsEnabled: false,
authRequired: false
});
expect(testServer.port).toEqual(2533);
});
tap.test('CCM-07: Automatic Reconnection - should reconnect after connection loss', async () => {
const client = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
// First connection and email
const email1 = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Before Disconnect',
text: 'First email before connection loss'
});
const result1 = await client.sendMail(email1);
expect(result1.success).toBeTrue();
expect(client.isConnected()).toBeTrue();
// Force disconnect
await client.close();
expect(client.isConnected()).toBeFalse();
// Try to send another email - should auto-reconnect
const email2 = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'After Reconnect',
text: 'Email after automatic reconnection'
});
const result2 = await client.sendMail(email2);
expect(result2.success).toBeTrue();
expect(client.isConnected()).toBeTrue();
await client.close();
console.log('✅ Automatic reconnection successful');
});
tap.test('CCM-07: Automatic Reconnection - pooled client should reconnect failed connections', async () => {
const pooledClient = createPooledSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
maxConnections: 3,
connectionTimeout: 5000,
debug: true
});
// Send emails to establish pool connections
const promises = [];
for (let i = 0; i < 3; i++) {
const email = new Email({
from: 'sender@example.com',
to: `recipient${i}@example.com`,
subject: `Pool Test ${i}`,
text: 'Testing connection pool'
});
promises.push(pooledClient.sendMail(email));
}
await Promise.all(promises);
const poolStatus1 = pooledClient.getPoolStatus();
console.log('📊 Pool status before disruption:', poolStatus1);
// Send more emails - pool should handle any connection issues
const promises2 = [];
for (let i = 0; i < 5; i++) {
const email = new Email({
from: 'sender@example.com',
to: `recipient${i}@example.com`,
subject: `Pool Recovery ${i}`,
text: 'Testing pool recovery'
});
promises2.push(pooledClient.sendMail(email));
}
const results = await Promise.all(promises2);
results.forEach(result => {
expect(result.success).toBeTrue();
});
const poolStatus2 = pooledClient.getPoolStatus();
console.log('📊 Pool status after recovery:', poolStatus2);
await pooledClient.close();
console.log('✅ Connection pool handles reconnection automatically');
});
tap.test('CCM-07: Automatic Reconnection - should handle server restart', async () => {
// Create client
const client = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000
});
// Send first email
const email1 = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Before Server Restart',
text: 'Email before server restart'
});
const result1 = await client.sendMail(email1);
expect(result1.success).toBeTrue();
// Simulate server restart
console.log('🔄 Simulating server restart...');
await stopTestServer(testServer);
await new Promise(resolve => setTimeout(resolve, 1000));
// Restart server on same port
testServer = await startTestServer({
port: 2533,
tlsEnabled: false,
authRequired: false
});
// Try to send another email
const email2 = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'After Server Restart',
text: 'Email after server restart'
});
const result2 = await client.sendMail(email2);
expect(result2.success).toBeTrue();
await client.close();
console.log('✅ Client recovered from server restart');
});
tap.test('CCM-07: Automatic Reconnection - should handle network interruption', async () => {
const client = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
socketTimeout: 10000
});
// Establish connection
await client.verify();
// Send emails with simulated network issues
for (let i = 0; i < 3; i++) {
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: `Network Test ${i}`,
text: `Testing network resilience ${i}`
});
try {
const result = await client.sendMail(email);
expect(result.success).toBeTrue();
console.log(`✅ Email ${i + 1} sent successfully`);
} catch (error) {
console.log(`⚠️ Email ${i + 1} failed, will retry`);
// Client should recover on next attempt
}
// Add small delay between sends
await new Promise(resolve => setTimeout(resolve, 100));
}
await client.close();
});
tap.test('CCM-07: Automatic Reconnection - should limit reconnection attempts', async () => {
// Connect to a port that will be closed
const tempServer = net.createServer();
await new Promise<void>((resolve) => {
tempServer.listen(2534, () => resolve());
});
const client = createSmtpClient({
host: 'localhost',
port: 2534,
secure: false,
connectionTimeout: 2000
});
// Close the server to simulate failure
tempServer.close();
await new Promise(resolve => setTimeout(resolve, 100));
let errorCount = 0;
const maxAttempts = 3;
// Try multiple times
for (let i = 0; i < maxAttempts; i++) {
try {
await client.verify();
} catch (error) {
errorCount++;
}
}
expect(errorCount).toEqual(maxAttempts);
console.log('✅ Reconnection attempts are limited to prevent infinite loops');
});
tap.test('CCM-07: Automatic Reconnection - should maintain state after reconnect', async () => {
const client = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000
});
// Send email with specific settings
const email1 = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'State Test 1',
text: 'Testing state persistence',
priority: 'high',
headers: {
'X-Test-ID': 'test-123'
}
});
const result1 = await client.sendMail(email1);
expect(result1.success).toBeTrue();
// Force reconnection
await client.close();
// Send another email - client state should be maintained
const email2 = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'State Test 2',
text: 'After reconnection',
priority: 'high',
headers: {
'X-Test-ID': 'test-456'
}
});
const result2 = await client.sendMail(email2);
expect(result2.success).toBeTrue();
await client.close();
console.log('✅ Client state maintained after reconnection');
});
tap.test('CCM-07: Automatic Reconnection - should handle rapid reconnections', async () => {
const client = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000
});
// Rapid connect/disconnect cycles
for (let i = 0; i < 5; i++) {
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: `Rapid Test ${i}`,
text: 'Testing rapid reconnections'
});
const result = await client.sendMail(email);
expect(result.success).toBeTrue();
// Force disconnect
await client.close();
// No delay - immediate next attempt
}
console.log('✅ Rapid reconnections handled successfully');
});
tap.test('cleanup - stop SMTP server', async () => {
await stopTestServer(testServer);
});
tap.start();

View File

@ -0,0 +1,135 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestSmtpServer } from '../../helpers/server.loader.js';
import * as dns from 'dns';
import { promisify } from 'util';
const resolveMx = promisify(dns.resolveMx);
const resolve4 = promisify(dns.resolve4);
const resolve6 = promisify(dns.resolve6);
let testServer: any;
tap.test('setup test SMTP server', async () => {
testServer = await startTestSmtpServer();
expect(testServer).toBeTruthy();
expect(testServer.port).toBeGreaterThan(0);
});
tap.test('CCM-08: DNS resolution and MX record lookup', async () => {
// Test basic DNS resolution
try {
const ipv4Addresses = await resolve4('example.com');
expect(ipv4Addresses).toBeArray();
expect(ipv4Addresses.length).toBeGreaterThan(0);
console.log('IPv4 addresses for example.com:', ipv4Addresses);
} catch (error) {
console.log('IPv4 resolution failed (may be expected in test environment):', error.message);
}
// Test IPv6 resolution
try {
const ipv6Addresses = await resolve6('example.com');
expect(ipv6Addresses).toBeArray();
console.log('IPv6 addresses for example.com:', ipv6Addresses);
} catch (error) {
console.log('IPv6 resolution failed (common for many domains):', error.message);
}
// Test MX record lookup
try {
const mxRecords = await resolveMx('example.com');
expect(mxRecords).toBeArray();
if (mxRecords.length > 0) {
expect(mxRecords[0]).toHaveProperty('priority');
expect(mxRecords[0]).toHaveProperty('exchange');
console.log('MX records for example.com:', mxRecords);
}
} catch (error) {
console.log('MX record lookup failed (may be expected in test environment):', error.message);
}
// Test local resolution (should work in test environment)
try {
const localhostIpv4 = await resolve4('localhost');
expect(localhostIpv4).toContain('127.0.0.1');
} catch (error) {
// Fallback for environments where localhost doesn't resolve via DNS
console.log('Localhost DNS resolution not available, using direct IP');
}
// Test invalid domain handling
try {
await resolve4('this-domain-definitely-does-not-exist-12345.com');
expect(true).toBeFalsy(); // Should not reach here
} catch (error) {
expect(error.code).toMatch(/ENOTFOUND|ENODATA/);
}
// Test MX record priority sorting
const mockMxRecords = [
{ priority: 20, exchange: 'mx2.example.com' },
{ priority: 10, exchange: 'mx1.example.com' },
{ priority: 30, exchange: 'mx3.example.com' }
];
const sortedRecords = mockMxRecords.sort((a, b) => a.priority - b.priority);
expect(sortedRecords[0].exchange).toEqual('mx1.example.com');
expect(sortedRecords[1].exchange).toEqual('mx2.example.com');
expect(sortedRecords[2].exchange).toEqual('mx3.example.com');
});
tap.test('CCM-08: DNS caching behavior', async () => {
const startTime = Date.now();
// First resolution (cold cache)
try {
await resolve4('example.com');
} catch (error) {
// Ignore errors, we're testing timing
}
const firstResolutionTime = Date.now() - startTime;
// Second resolution (potentially cached)
const secondStartTime = Date.now();
try {
await resolve4('example.com');
} catch (error) {
// Ignore errors, we're testing timing
}
const secondResolutionTime = Date.now() - secondStartTime;
console.log(`First resolution: ${firstResolutionTime}ms, Second resolution: ${secondResolutionTime}ms`);
// Note: We can't guarantee caching behavior in all environments
// so we just log the times for manual inspection
});
tap.test('CCM-08: Multiple A record handling', async () => {
// Test handling of domains with multiple A records
try {
const googleIps = await resolve4('google.com');
if (googleIps.length > 1) {
expect(googleIps).toBeArray();
expect(googleIps.length).toBeGreaterThan(1);
console.log('Multiple A records found for google.com:', googleIps);
// Verify all are valid IPv4 addresses
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
for (const ip of googleIps) {
expect(ip).toMatch(ipv4Regex);
}
}
} catch (error) {
console.log('Could not resolve google.com:', error.message);
}
});
tap.test('cleanup test SMTP server', async () => {
if (testServer) {
await testServer.stop();
}
});
export default tap.start();

View File

@ -0,0 +1,201 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestSmtpServer } from '../../helpers/server.loader.js';
import { createSmtpClient } from '../../helpers/smtp.client.js';
import * as net from 'net';
import * as os from 'os';
let testServer: any;
tap.test('setup test SMTP server', async () => {
testServer = await startTestSmtpServer();
expect(testServer).toBeTruthy();
expect(testServer.port).toBeGreaterThan(0);
});
tap.test('CCM-09: Check system IPv6 support', async () => {
const networkInterfaces = os.networkInterfaces();
let hasIPv6 = false;
for (const interfaceName in networkInterfaces) {
const interfaces = networkInterfaces[interfaceName];
if (interfaces) {
for (const iface of interfaces) {
if (iface.family === 'IPv6' && !iface.internal) {
hasIPv6 = true;
console.log(`Found IPv6 address: ${iface.address} on ${interfaceName}`);
}
}
}
}
console.log(`System has IPv6 support: ${hasIPv6}`);
});
tap.test('CCM-09: IPv4 connection test', async () => {
const smtpClient = createSmtpClient({
host: '127.0.0.1', // Explicit IPv4
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
let connected = false;
let connectionFamily = '';
smtpClient.on('connection', (info: any) => {
connected = true;
if (info && info.socket) {
connectionFamily = info.socket.remoteFamily || '';
}
});
smtpClient.on('error', (error: Error) => {
console.error('IPv4 connection error:', error.message);
});
// Test connection
const result = await smtpClient.connect();
expect(result).toBeTruthy();
expect(smtpClient.isConnected()).toBeTruthy();
console.log(`Connected via IPv4, family: ${connectionFamily}`);
await smtpClient.close();
});
tap.test('CCM-09: IPv6 connection test (if supported)', async () => {
// Check if IPv6 is available
const hasIPv6 = await new Promise<boolean>((resolve) => {
const testSocket = net.createConnection({
host: '::1',
port: 1, // Any port, will fail but tells us if IPv6 works
timeout: 100
});
testSocket.on('error', (err: any) => {
// ECONNREFUSED means IPv6 works but port is closed (expected)
// ENETUNREACH or EAFNOSUPPORT means IPv6 not available
resolve(err.code === 'ECONNREFUSED');
});
testSocket.on('connect', () => {
testSocket.end();
resolve(true);
});
});
if (!hasIPv6) {
console.log('IPv6 not available on this system, skipping IPv6 tests');
return;
}
// Try IPv6 connection
const smtpClient = createSmtpClient({
host: '::1', // IPv6 loopback
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
let connected = false;
let connectionFamily = '';
smtpClient.on('connection', (info: any) => {
connected = true;
if (info && info.socket) {
connectionFamily = info.socket.remoteFamily || '';
}
});
smtpClient.on('error', (error: Error) => {
console.error('IPv6 connection error:', error.message);
});
try {
const result = await smtpClient.connect();
if (result && smtpClient.isConnected()) {
console.log(`Connected via IPv6, family: ${connectionFamily}`);
await smtpClient.close();
}
} catch (error) {
console.log('IPv6 connection failed (server may not support IPv6):', error.message);
}
});
tap.test('CCM-09: Hostname resolution preference', async () => {
// Test that client can handle hostnames that resolve to both IPv4 and IPv6
const smtpClient = createSmtpClient({
host: 'localhost', // Should resolve to both 127.0.0.1 and ::1
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
let connectionInfo: any = null;
smtpClient.on('connection', (info: any) => {
connectionInfo = info;
});
const result = await smtpClient.connect();
expect(result).toBeTruthy();
expect(smtpClient.isConnected()).toBeTruthy();
if (connectionInfo && connectionInfo.socket) {
console.log(`Connected to localhost via ${connectionInfo.socket.remoteFamily || 'unknown'}`);
console.log(`Local address: ${connectionInfo.socket.localAddress}`);
console.log(`Remote address: ${connectionInfo.socket.remoteAddress}`);
}
await smtpClient.close();
});
tap.test('CCM-09: Happy Eyeballs algorithm simulation', async () => {
// Test connecting to multiple addresses with preference
const addresses = ['127.0.0.1', '::1', 'localhost'];
const results: Array<{ address: string; time: number; success: boolean }> = [];
for (const address of addresses) {
const startTime = Date.now();
const smtpClient = createSmtpClient({
host: address,
port: testServer.port,
secure: false,
connectionTimeout: 1000,
debug: false
});
try {
const connected = await smtpClient.connect();
const elapsed = Date.now() - startTime;
results.push({ address, time: elapsed, success: !!connected });
if (connected) {
await smtpClient.close();
}
} catch (error) {
const elapsed = Date.now() - startTime;
results.push({ address, time: elapsed, success: false });
}
}
console.log('Connection race results:');
results.forEach(r => {
console.log(` ${r.address}: ${r.success ? 'SUCCESS' : 'FAILED'} in ${r.time}ms`);
});
// At least one should succeed
const successfulConnections = results.filter(r => r.success);
expect(successfulConnections.length).toBeGreaterThan(0);
});
tap.test('cleanup test SMTP server', async () => {
if (testServer) {
await testServer.stop();
}
});
export default tap.start();

View File

@ -0,0 +1,298 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestSmtpServer } from '../../helpers/server.loader.js';
import { createSmtpClient } from '../../helpers/smtp.client.js';
import * as net from 'net';
import * as http from 'http';
let testServer: any;
let proxyServer: http.Server;
let socksProxyServer: net.Server;
tap.test('setup test SMTP server', async () => {
testServer = await startTestSmtpServer();
expect(testServer).toBeTruthy();
expect(testServer.port).toBeGreaterThan(0);
});
tap.test('CCM-10: Setup HTTP CONNECT proxy', async () => {
// Create a simple HTTP CONNECT proxy
proxyServer = http.createServer();
proxyServer.on('connect', (req, clientSocket, head) => {
console.log(`Proxy CONNECT request to ${req.url}`);
const [host, port] = req.url!.split(':');
const serverSocket = net.connect(parseInt(port), host, () => {
clientSocket.write('HTTP/1.1 200 Connection Established\r\n' +
'Proxy-agent: Test-Proxy\r\n' +
'\r\n');
// Pipe data between client and server
serverSocket.pipe(clientSocket);
clientSocket.pipe(serverSocket);
});
serverSocket.on('error', (err) => {
console.error('Proxy server socket error:', err);
clientSocket.end();
});
clientSocket.on('error', (err) => {
console.error('Proxy client socket error:', err);
serverSocket.end();
});
});
await new Promise<void>((resolve) => {
proxyServer.listen(0, '127.0.0.1', () => {
const address = proxyServer.address() as net.AddressInfo;
console.log(`HTTP proxy listening on port ${address.port}`);
resolve();
});
});
});
tap.test('CCM-10: Test connection through HTTP proxy', async () => {
const proxyAddress = proxyServer.address() as net.AddressInfo;
// Note: Real SMTP clients would need proxy configuration
// This simulates what a proxy-aware SMTP client would do
const proxyOptions = {
host: proxyAddress.address,
port: proxyAddress.port,
method: 'CONNECT',
path: `127.0.0.1:${testServer.port}`,
headers: {
'Proxy-Authorization': 'Basic dGVzdDp0ZXN0' // test:test in base64
}
};
const connected = await new Promise<boolean>((resolve) => {
const req = http.request(proxyOptions);
req.on('connect', (res, socket, head) => {
console.log('Connected through proxy, status:', res.statusCode);
expect(res.statusCode).toEqual(200);
// Now we have a raw socket to the SMTP server through the proxy
socket.on('data', (data) => {
const response = data.toString();
console.log('SMTP response through proxy:', response.trim());
if (response.includes('220')) {
socket.write('QUIT\r\n');
socket.end();
resolve(true);
}
});
socket.on('error', (err) => {
console.error('Socket error:', err);
resolve(false);
});
});
req.on('error', (err) => {
console.error('Proxy request error:', err);
resolve(false);
});
req.end();
});
expect(connected).toBeTruthy();
});
tap.test('CCM-10: Test SOCKS5 proxy simulation', async () => {
// Create a minimal SOCKS5 proxy for testing
socksProxyServer = net.createServer((clientSocket) => {
let authenticated = false;
let targetHost: string;
let targetPort: number;
clientSocket.on('data', (data) => {
if (!authenticated) {
// SOCKS5 handshake
if (data[0] === 0x05) { // SOCKS version 5
// Send back: no authentication required
clientSocket.write(Buffer.from([0x05, 0x00]));
authenticated = true;
}
} else if (!targetHost) {
// Connection request
if (data[0] === 0x05 && data[1] === 0x01) { // CONNECT command
const addressType = data[3];
if (addressType === 0x01) { // IPv4
targetHost = `${data[4]}.${data[5]}.${data[6]}.${data[7]}`;
targetPort = (data[8] << 8) + data[9];
// Connect to target
const serverSocket = net.connect(targetPort, targetHost, () => {
// Send success response
const response = Buffer.alloc(10);
response[0] = 0x05; // SOCKS version
response[1] = 0x00; // Success
response[2] = 0x00; // Reserved
response[3] = 0x01; // IPv4
response[4] = data[4]; // Copy address
response[5] = data[5];
response[6] = data[6];
response[7] = data[7];
response[8] = data[8]; // Copy port
response[9] = data[9];
clientSocket.write(response);
// Start proxying
serverSocket.pipe(clientSocket);
clientSocket.pipe(serverSocket);
});
serverSocket.on('error', (err) => {
console.error('SOCKS target connection error:', err);
clientSocket.end();
});
}
}
}
});
clientSocket.on('error', (err) => {
console.error('SOCKS client error:', err);
});
});
await new Promise<void>((resolve) => {
socksProxyServer.listen(0, '127.0.0.1', () => {
const address = socksProxyServer.address() as net.AddressInfo;
console.log(`SOCKS5 proxy listening on port ${address.port}`);
resolve();
});
});
// Test connection through SOCKS proxy
const socksAddress = socksProxyServer.address() as net.AddressInfo;
const socksClient = net.connect(socksAddress.port, socksAddress.address);
const connected = await new Promise<boolean>((resolve) => {
let phase = 'handshake';
socksClient.on('connect', () => {
// Send SOCKS5 handshake
socksClient.write(Buffer.from([0x05, 0x01, 0x00])); // Version 5, 1 method, no auth
});
socksClient.on('data', (data) => {
if (phase === 'handshake' && data[0] === 0x05 && data[1] === 0x00) {
phase = 'connect';
// Send connection request
const connectReq = Buffer.alloc(10);
connectReq[0] = 0x05; // SOCKS version
connectReq[1] = 0x01; // CONNECT
connectReq[2] = 0x00; // Reserved
connectReq[3] = 0x01; // IPv4
connectReq[4] = 127; // 127.0.0.1
connectReq[5] = 0;
connectReq[6] = 0;
connectReq[7] = 1;
connectReq[8] = (testServer.port >> 8) & 0xFF; // Port high byte
connectReq[9] = testServer.port & 0xFF; // Port low byte
socksClient.write(connectReq);
} else if (phase === 'connect' && data[0] === 0x05 && data[1] === 0x00) {
phase = 'connected';
console.log('Connected through SOCKS5 proxy');
// Now we're connected to the SMTP server
} else if (phase === 'connected') {
const response = data.toString();
console.log('SMTP response through SOCKS:', response.trim());
if (response.includes('220')) {
socksClient.write('QUIT\r\n');
socksClient.end();
resolve(true);
}
}
});
socksClient.on('error', (err) => {
console.error('SOCKS client error:', err);
resolve(false);
});
setTimeout(() => resolve(false), 5000); // Timeout after 5 seconds
});
expect(connected).toBeTruthy();
});
tap.test('CCM-10: Test proxy authentication failure', async () => {
// Create a proxy that requires authentication
const authProxyServer = http.createServer();
authProxyServer.on('connect', (req, clientSocket, head) => {
const authHeader = req.headers['proxy-authorization'];
if (!authHeader || authHeader !== 'Basic dGVzdDp0ZXN0') {
clientSocket.write('HTTP/1.1 407 Proxy Authentication Required\r\n' +
'Proxy-Authenticate: Basic realm="Test Proxy"\r\n' +
'\r\n');
clientSocket.end();
return;
}
// Authentication successful, proceed with connection
const [host, port] = req.url!.split(':');
const serverSocket = net.connect(parseInt(port), host, () => {
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
serverSocket.pipe(clientSocket);
clientSocket.pipe(serverSocket);
});
});
await new Promise<void>((resolve) => {
authProxyServer.listen(0, '127.0.0.1', () => {
resolve();
});
});
const authProxyAddress = authProxyServer.address() as net.AddressInfo;
// Test without authentication
const failedAuth = await new Promise<boolean>((resolve) => {
const req = http.request({
host: authProxyAddress.address,
port: authProxyAddress.port,
method: 'CONNECT',
path: `127.0.0.1:${testServer.port}`
});
req.on('connect', () => resolve(false));
req.on('response', (res) => {
expect(res.statusCode).toEqual(407);
resolve(true);
});
req.on('error', () => resolve(false));
req.end();
});
expect(failedAuth).toBeTruthy();
authProxyServer.close();
});
tap.test('cleanup test servers', async () => {
if (proxyServer) {
await new Promise<void>((resolve) => proxyServer.close(() => resolve()));
}
if (socksProxyServer) {
await new Promise<void>((resolve) => socksProxyServer.close(() => resolve()));
}
if (testServer) {
await testServer.stop();
}
});
export default tap.start();

View File

@ -0,0 +1,284 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestSmtpServer } from '../../helpers/server.loader.js';
import { createSmtpClient } from '../../helpers/smtp.client.js';
import * as net from 'net';
let testServer: any;
tap.test('setup test SMTP server', async () => {
testServer = await startTestSmtpServer({
socketTimeout: 30000 // 30 second timeout for keep-alive tests
});
expect(testServer).toBeTruthy();
expect(testServer.port).toBeGreaterThan(0);
});
tap.test('CCM-11: Basic keep-alive functionality', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
keepAlive: true,
keepAliveInterval: 5000, // 5 seconds
connectionTimeout: 10000,
debug: true
});
// Connect to server
const connected = await smtpClient.connect();
expect(connected).toBeTruthy();
expect(smtpClient.isConnected()).toBeTruthy();
// Track keep-alive activity
let keepAliveCount = 0;
let lastActivity = Date.now();
smtpClient.on('keepalive', () => {
keepAliveCount++;
const elapsed = Date.now() - lastActivity;
console.log(`Keep-alive sent after ${elapsed}ms`);
lastActivity = Date.now();
});
// Wait for multiple keep-alive cycles
await new Promise(resolve => setTimeout(resolve, 12000)); // Wait 12 seconds
// Should have sent at least 2 keep-alive messages
expect(keepAliveCount).toBeGreaterThanOrEqual(2);
// Connection should still be alive
expect(smtpClient.isConnected()).toBeTruthy();
await smtpClient.close();
});
tap.test('CCM-11: Keep-alive with NOOP command', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
keepAlive: true,
keepAliveInterval: 3000,
connectionTimeout: 10000,
debug: true
});
await smtpClient.connect();
let noopResponses = 0;
// Send NOOP commands manually to simulate keep-alive
for (let i = 0; i < 3; i++) {
await new Promise(resolve => setTimeout(resolve, 2000));
try {
const response = await smtpClient.sendCommand('NOOP');
if (response && response.includes('250')) {
noopResponses++;
console.log(`NOOP response ${i + 1}: ${response.trim()}`);
}
} catch (error) {
console.error('NOOP error:', error.message);
}
}
expect(noopResponses).toEqual(3);
expect(smtpClient.isConnected()).toBeTruthy();
await smtpClient.close();
});
tap.test('CCM-11: Connection idle timeout without keep-alive', async () => {
// Create a client without keep-alive
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
keepAlive: false, // Disabled
connectionTimeout: 5000,
socketTimeout: 5000, // 5 second socket timeout
debug: true
});
await smtpClient.connect();
expect(smtpClient.isConnected()).toBeTruthy();
let disconnected = false;
let timeoutError = false;
smtpClient.on('timeout', () => {
timeoutError = true;
console.log('Socket timeout detected');
});
smtpClient.on('close', () => {
disconnected = true;
console.log('Connection closed');
});
smtpClient.on('error', (error: Error) => {
console.log('Connection error:', error.message);
if (error.message.includes('timeout')) {
timeoutError = true;
}
});
// Wait for timeout (longer than socket timeout)
await new Promise(resolve => setTimeout(resolve, 7000));
// Without keep-alive, connection might timeout
// This depends on server configuration
if (disconnected || timeoutError) {
console.log('Connection timed out as expected without keep-alive');
expect(disconnected || timeoutError).toBeTruthy();
} else {
// Some servers might not timeout quickly
console.log('Server did not timeout connection (may have long timeout setting)');
await smtpClient.close();
}
});
tap.test('CCM-11: Keep-alive during long operations', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
keepAlive: true,
keepAliveInterval: 2000,
connectionTimeout: 10000,
debug: true
});
await smtpClient.connect();
// Simulate a long operation
console.log('Starting simulated long operation...');
// Send initial MAIL FROM
await smtpClient.sendCommand('MAIL FROM:<sender@example.com>');
// Track keep-alives during operation
let keepAliveDuringOperation = 0;
smtpClient.on('keepalive', () => {
keepAliveDuringOperation++;
});
// Simulate processing delay
await new Promise(resolve => setTimeout(resolve, 5000));
// Continue with RCPT TO
await smtpClient.sendCommand('RCPT TO:<recipient@example.com>');
// More delay
await new Promise(resolve => setTimeout(resolve, 3000));
// Should have sent keep-alives during delays
expect(keepAliveDuringOperation).toBeGreaterThan(0);
console.log(`Sent ${keepAliveDuringOperation} keep-alives during operation`);
// Reset the session
await smtpClient.sendCommand('RSET');
await smtpClient.close();
});
tap.test('CCM-11: Keep-alive interval adjustment', async () => {
const intervals = [1000, 3000, 5000]; // Different intervals to test
for (const interval of intervals) {
console.log(`\nTesting keep-alive with ${interval}ms interval`);
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
keepAlive: true,
keepAliveInterval: interval,
connectionTimeout: 10000,
debug: false // Less verbose for this test
});
await smtpClient.connect();
let keepAliveCount = 0;
let keepAliveTimes: number[] = [];
let lastTime = Date.now();
smtpClient.on('keepalive', () => {
const now = Date.now();
const elapsed = now - lastTime;
keepAliveTimes.push(elapsed);
lastTime = now;
keepAliveCount++;
});
// Wait for multiple intervals
await new Promise(resolve => setTimeout(resolve, interval * 3.5));
// Should have sent approximately 3 keep-alives
expect(keepAliveCount).toBeGreaterThanOrEqual(2);
expect(keepAliveCount).toBeLessThanOrEqual(4);
// Check interval accuracy (allowing 20% variance)
const avgInterval = keepAliveTimes.reduce((a, b) => a + b, 0) / keepAliveTimes.length;
expect(avgInterval).toBeGreaterThan(interval * 0.8);
expect(avgInterval).toBeLessThan(interval * 1.2);
console.log(`Sent ${keepAliveCount} keep-alives, avg interval: ${avgInterval.toFixed(0)}ms`);
await smtpClient.close();
}
});
tap.test('CCM-11: TCP keep-alive socket options', async () => {
// Test low-level TCP keep-alive options
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
socketOptions: {
keepAlive: true,
keepAliveInitialDelay: 1000
},
connectionTimeout: 10000,
debug: true
});
let socketConfigured = false;
smtpClient.on('connection', (info: any) => {
if (info && info.socket && info.socket instanceof net.Socket) {
// Check if keep-alive is enabled at socket level
const socket = info.socket as net.Socket;
// These methods might not be available in all Node versions
if (typeof socket.setKeepAlive === 'function') {
socket.setKeepAlive(true, 1000);
socketConfigured = true;
console.log('TCP keep-alive configured at socket level');
}
}
});
await smtpClient.connect();
// Wait a bit to ensure socket options take effect
await new Promise(resolve => setTimeout(resolve, 2000));
expect(smtpClient.isConnected()).toBeTruthy();
if (!socketConfigured) {
console.log('Socket-level keep-alive configuration not available');
}
await smtpClient.close();
});
tap.test('cleanup test SMTP server', async () => {
if (testServer) {
await testServer.stop();
}
});
export default tap.start();