import { tap, expect } from '@git.zone/tstest/tapbundle'; import * as net from 'net'; import * as tls from 'tls'; import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; import type { SmtpServer } from '../../../ts/mail/delivery/smtpserver/index.js'; const TEST_PORT = 2525; const TEST_PORT_TLS = 30465; const TEST_TIMEOUT = 30000; let testServer: SmtpServer; let testServerTls: ITestServer; tap.test('setup - start SMTP servers for TLS version tests', async () => { testServer = await startTestServer(); await new Promise(resolve => setTimeout(resolve, 1000));testServerTls = await startTestServer(); await new Promise(resolve => setTimeout(resolve, 1000)); expect(testServerTls).toBeInstanceOf(Object); }); tap.test('TLS Versions - should support STARTTLS capability', async (tools) => { const done = tools.defer(); try { const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: TEST_TIMEOUT }); await new Promise((resolve, reject) => { socket.once('connect', () => resolve()); socket.once('error', reject); }); // Get banner await new Promise((resolve) => { socket.once('data', (chunk) => resolve(chunk.toString())); }); // Send EHLO socket.write('EHLO testhost\r\n'); const ehloResponse = await new Promise((resolve) => { let data = ''; const handler = (chunk: Buffer) => { data += chunk.toString(); if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) { socket.removeListener('data', handler); resolve(data); } }; socket.on('data', handler); }); console.log('EHLO response:', ehloResponse); // Check for STARTTLS support const supportsStarttls = ehloResponse.includes('250-STARTTLS') || ehloResponse.includes('250 STARTTLS'); console.log('STARTTLS supported:', supportsStarttls); if (supportsStarttls) { // Test STARTTLS upgrade socket.write('STARTTLS\r\n'); const starttlsResponse = await new Promise((resolve) => { socket.once('data', (chunk) => resolve(chunk.toString())); }); expect(starttlsResponse).toInclude('220'); console.log('STARTTLS ready response received'); // Would upgrade to TLS here in a real implementation // For testing, we just verify the capability } // Clean up socket.write('QUIT\r\n'); socket.end(); // STARTTLS is optional but common expect(true).toEqual(true); } finally { done.resolve(); } }); tap.test('TLS Versions - should support modern TLS versions on secure port', async (tools) => { const done = tools.defer(); try { // Test TLS 1.2 console.log('Testing TLS 1.2 support...'); const tls12Result = await testTlsVersion('TLSv1.2', TEST_PORT_TLS); console.log('TLS 1.2 result:', tls12Result); // Test TLS 1.3 console.log('Testing TLS 1.3 support...'); const tls13Result = await testTlsVersion('TLSv1.3', TEST_PORT_TLS); console.log('TLS 1.3 result:', tls13Result); // At least one modern version should be supported const supportsModernTls = tls12Result.success || tls13Result.success; expect(supportsModernTls).toEqual(true); if (tls12Result.success) { console.log('TLS 1.2 supported with cipher:', tls12Result.cipher); } if (tls13Result.success) { console.log('TLS 1.3 supported with cipher:', tls13Result.cipher); } } finally { done.resolve(); } }); tap.test('TLS Versions - should reject obsolete TLS versions', async (tools) => { const done = tools.defer(); try { // Test TLS 1.0 (should be rejected by modern servers) console.log('Testing TLS 1.0 (obsolete)...'); const tls10Result = await testTlsVersion('TLSv1', TEST_PORT_TLS); // Test TLS 1.1 (should be rejected by modern servers) console.log('Testing TLS 1.1 (obsolete)...'); const tls11Result = await testTlsVersion('TLSv1.1', TEST_PORT_TLS); // Modern servers should reject these old versions // But some might still support them for compatibility console.log(`TLS 1.0 ${tls10Result.success ? 'accepted (legacy support)' : 'rejected (good)'}`); console.log(`TLS 1.1 ${tls11Result.success ? 'accepted (legacy support)' : 'rejected (good)'}`); // Either behavior is acceptable - log the results expect(true).toEqual(true); } finally { done.resolve(); } }); tap.test('TLS Versions - should provide cipher information', async (tools) => { const done = tools.defer(); try { const tlsOptions = { host: 'localhost', port: TEST_PORT_TLS, rejectUnauthorized: false, timeout: TEST_TIMEOUT }; const socket = await new Promise((resolve, reject) => { const tlsSocket = tls.connect(tlsOptions, () => { resolve(tlsSocket); }); tlsSocket.on('error', reject); setTimeout(() => reject(new Error('TLS connection timeout')), 5000); }); // Get connection details const cipher = socket.getCipher(); const protocol = socket.getProtocol(); const authorized = socket.authorized; console.log('TLS connection established:'); console.log('- Protocol:', protocol); console.log('- Cipher:', cipher.name); console.log('- Key exchange:', cipher.standardName); console.log('- Authorized:', authorized); expect(protocol).toBeDefined(); expect(cipher.name).toBeDefined(); // Send SMTP greeting to verify encrypted connection works const banner = await new Promise((resolve) => { socket.once('data', (chunk) => resolve(chunk.toString())); }); expect(banner).toInclude('220'); console.log('Received SMTP banner over TLS'); // Clean up socket.write('QUIT\r\n'); socket.end(); } finally { done.resolve(); } }); // Helper function to test specific TLS version async function testTlsVersion(version: string, port: number): Promise<{success: boolean, cipher?: any, error?: string}> { return new Promise((resolve) => { const tlsOptions: any = { host: 'localhost', port: port, rejectUnauthorized: false, timeout: 5000 }; // Set version constraints based on requested version switch (version) { case 'TLSv1': tlsOptions.minVersion = 'TLSv1'; tlsOptions.maxVersion = 'TLSv1'; break; case 'TLSv1.1': tlsOptions.minVersion = 'TLSv1.1'; tlsOptions.maxVersion = 'TLSv1.1'; break; case 'TLSv1.2': tlsOptions.minVersion = 'TLSv1.2'; tlsOptions.maxVersion = 'TLSv1.2'; break; case 'TLSv1.3': tlsOptions.minVersion = 'TLSv1.3'; tlsOptions.maxVersion = 'TLSv1.3'; break; } const socket = tls.connect(tlsOptions, () => { const cipher = socket.getCipher(); const protocol = socket.getProtocol(); socket.destroy(); resolve({ success: true, cipher: { name: cipher.name, standardName: cipher.standardName, protocol: protocol } }); }); socket.on('error', (error) => { resolve({ success: false, error: error.message }); }); setTimeout(() => { socket.destroy(); resolve({ success: false, error: 'Connection timeout' }); }, 5000); }); } tap.test('cleanup - stop SMTP servers', async () => { await stopTestServer(); await stopTestServer(testServerTls); expect(true).toEqual(true); }); tap.start();