import { tap, expect } from '@git.zone/tstest/tapbundle'; import * as net from 'net'; import * as tls from 'tls'; import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js'; const TEST_PORT = 2525; const TEST_TIMEOUT = 30000; let testServer: ITestServer; tap.test('setup - start SMTP server with TLS support for version tests', async () => { testServer = await startTestServer({ port: TEST_PORT, tlsEnabled: true }); await new Promise(resolve => setTimeout(resolve, 1000)); expect(testServer).toBeDefined(); }); 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 via STARTTLS', async (tools) => { const done = tools.defer(); try { // Test TLS 1.2 via STARTTLS console.log('Testing TLS 1.2 support via STARTTLS...'); const tls12Result = await testTlsVersionViaStartTls('TLSv1.2', TEST_PORT); console.log('TLS 1.2 result:', tls12Result); // Test TLS 1.3 via STARTTLS console.log('Testing TLS 1.3 support via STARTTLS...'); const tls13Result = await testTlsVersionViaStartTls('TLSv1.3', TEST_PORT); 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 via STARTTLS', async (tools) => { const done = tools.defer(); try { // Test TLS 1.0 (should be rejected by modern servers) console.log('Testing TLS 1.0 (obsolete) via STARTTLS...'); const tls10Result = await testTlsVersionViaStartTls('TLSv1', TEST_PORT); // Test TLS 1.1 (should be rejected by modern servers) console.log('Testing TLS 1.1 (obsolete) via STARTTLS...'); const tls11Result = await testTlsVersionViaStartTls('TLSv1.1', TEST_PORT); // 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 via STARTTLS', async (tools) => { const done = tools.defer(); try { // Connect to plain SMTP port 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); }); // Check for STARTTLS if (!ehloResponse.includes('STARTTLS')) { console.log('Server does not support STARTTLS - skipping cipher info test'); socket.write('QUIT\r\n'); socket.end(); done.resolve(); return; } // Send STARTTLS socket.write('STARTTLS\r\n'); await new Promise((resolve) => { socket.once('data', (chunk) => resolve(chunk.toString())); }); // Upgrade to TLS const tlsSocket = tls.connect({ socket: socket, servername: 'localhost', rejectUnauthorized: false }); await new Promise((resolve, reject) => { tlsSocket.once('secureConnect', () => resolve()); tlsSocket.once('error', reject); }); // Get connection details const cipher = tlsSocket.getCipher(); const protocol = tlsSocket.getProtocol(); const authorized = tlsSocket.authorized; console.log('TLS connection established via STARTTLS:'); console.log('- Protocol:', protocol); console.log('- Cipher:', cipher?.name); console.log('- Key exchange:', cipher?.standardName); console.log('- Authorized:', authorized); if (protocol) { expect(typeof protocol).toEqual('string'); } if (cipher) { expect(cipher.name).toBeDefined(); } // Clean up tlsSocket.write('QUIT\r\n'); tlsSocket.end(); } finally { done.resolve(); } }); // Helper function to test specific TLS version via STARTTLS async function testTlsVersionViaStartTls(version: string, port: number): Promise<{success: boolean, cipher?: any, error?: string}> { return new Promise(async (resolve) => { try { // Connect to plain SMTP port const socket = net.createConnection({ host: 'localhost', port: port, timeout: 5000 }); await new Promise((socketResolve, socketReject) => { socket.once('connect', () => socketResolve()); socket.once('error', socketReject); }); // Get banner await new Promise((bannerResolve) => { socket.once('data', (chunk) => bannerResolve(chunk.toString())); }); // Send EHLO socket.write('EHLO testhost\r\n'); const ehloResponse = await new Promise((ehloResolve) => { 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); ehloResolve(data); } }; socket.on('data', handler); }); // Check for STARTTLS if (!ehloResponse.includes('STARTTLS')) { socket.destroy(); resolve({ success: false, error: 'STARTTLS not supported' }); return; } // Send STARTTLS socket.write('STARTTLS\r\n'); await new Promise((starttlsResolve) => { socket.once('data', (chunk) => starttlsResolve(chunk.toString())); }); // Set up TLS options with version constraints const tlsOptions: any = { socket: socket, servername: 'localhost', rejectUnauthorized: false }; // 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; } // Upgrade to TLS const tlsSocket = tls.connect(tlsOptions); tlsSocket.once('secureConnect', () => { const cipher = tlsSocket.getCipher(); const protocol = tlsSocket.getProtocol(); tlsSocket.destroy(); resolve({ success: true, cipher: { name: cipher?.name, standardName: cipher?.standardName, protocol: protocol } }); }); tlsSocket.once('error', (error) => { resolve({ success: false, error: error.message }); }); setTimeout(() => { tlsSocket.destroy(); resolve({ success: false, error: 'TLS handshake timeout' }); }, 5000); } catch (error) { resolve({ success: false, error: error instanceof Error ? error.message : 'Unknown error' }); } }); } tap.test('cleanup - stop SMTP server', async () => { await stopTestServer(testServer); expect(true).toEqual(true); }); export default tap.start();