import { tap, expect } from '@git.zone/tstest/tapbundle'; import * as plugins from '../../../ts/plugins.js'; import * as net from 'net'; import { startTestServer, stopTestServer } from '../../helpers/server.loader.js' import type { ITestServer } from '../../helpers/server.loader.js'; const TEST_PORT = 2525; let testServer: ITestServer; // Helper function to wait for SMTP response const waitForResponse = (socket: net.Socket, expectedCode?: string, timeout = 5000): Promise => { return new Promise((resolve, reject) => { let buffer = ''; const timer = setTimeout(() => { socket.removeListener('data', handler); reject(new Error(`Timeout waiting for ${expectedCode || 'any'} response`)); }, timeout); const handler = (data: Buffer) => { buffer += data.toString(); const lines = buffer.split('\r\n'); // Check if we have a complete response for (const line of lines) { if (expectedCode) { if (line.startsWith(expectedCode + ' ')) { clearTimeout(timer); socket.removeListener('data', handler); resolve(buffer); return; } } else { // Any complete response line if (line.match(/^\d{3} /)) { clearTimeout(timer); socket.removeListener('data', handler); resolve(buffer); return; } } } }; socket.on('data', handler); }); }; tap.test('setup - start test server', async (toolsArg) => { testServer = await startTestServer({ port: TEST_PORT }); await toolsArg.delayFor(1000); }); tap.test('RFC 6376 DKIM - Server accepts email with DKIM signature', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 30000 }); socket.on('error', (err) => { console.error('Socket error:', err); done.reject(err); }); socket.on('connect', async () => { try { // Wait for greeting await waitForResponse(socket, '220'); // Send EHLO socket.write('EHLO testclient\r\n'); await waitForResponse(socket, '250'); // Send MAIL FROM socket.write('MAIL FROM:\r\n'); await waitForResponse(socket, '250'); // Send RCPT TO socket.write('RCPT TO:\r\n'); await waitForResponse(socket, '250'); // Send DATA socket.write('DATA\r\n'); await waitForResponse(socket, '354'); // Create email with DKIM signature const dkimSignature = [ 'DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;', ' d=example.com; s=default;', ' h=from:to:subject:date:message-id;', ' bh=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN/XKdLCPjaYaY=;', ' b=Kt1zLCYmUVYJKEOVL9nGF2JVPJ5/k5l6yOkNBJGCrZn4E5z9Qn7TlYrG8QfBgJ4', ' CzYVLjKm5xOhUoEaDzTJ1E6C9A4hL8sKfBxQjN8oWv4kP3GdE6mFqS0wKcRjT+', ' NxOz2VcJP4LmKjFsG8XqBhYoEfCvSr3UwNmEkP6RjT9WlQzA4kJe2VoMsJ=' ].join('\r\n'); const email = [ `From: sender@example.com`, `To: recipient@example.com`, `Subject: DKIM RFC 6376 Compliance Test`, `Date: ${new Date().toUTCString()}`, `Message-ID: `, dkimSignature, '', 'This email tests RFC 6376 DKIM compliance.', 'The server should properly handle DKIM signatures.', '.', '' ].join('\r\n'); socket.write(email); await waitForResponse(socket, '250'); console.log('Email with DKIM signature accepted'); expect(true).toEqual(true); // Server accepts DKIM headers // Send QUIT socket.write('QUIT\r\n'); await waitForResponse(socket, '221'); socket.end(); done.resolve(); } catch (err) { console.error('Test error:', err); socket.end(); done.reject(err); } }); await done.promise; }); tap.test('RFC 6376 DKIM - Multiple DKIM signatures', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 30000 }); socket.on('error', (err) => { console.error('Socket error:', err); done.reject(err); }); socket.on('connect', async () => { try { // Wait for greeting await waitForResponse(socket, '220'); // Send EHLO socket.write('EHLO testclient\r\n'); await waitForResponse(socket, '250'); // Send MAIL FROM socket.write('MAIL FROM:\r\n'); await waitForResponse(socket, '250'); // Send RCPT TO socket.write('RCPT TO:\r\n'); await waitForResponse(socket, '250'); // Send DATA socket.write('DATA\r\n'); await waitForResponse(socket, '354'); // Email with multiple DKIM signatures (common in forwarding scenarios) const email = [ `From: sender@example.com`, `To: recipient@example.com`, `Subject: Multiple DKIM Signatures Test`, `Date: ${new Date().toUTCString()}`, `Message-ID: `, 'DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;', ' d=example.com; s=selector1;', ' h=from:to:subject:date;', ' bh=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN/XKdLCPjaYaY=;', ' b=signature1data', 'DKIM-Signature: v=1; a=rsa-sha256; c=simple/simple;', ' d=forwarder.com; s=selector2;', ' h=from:to:subject:date:message-id;', ' bh=differentbodyhash=;', ' b=signature2data', '', 'Email with multiple DKIM signatures.', '.', '' ].join('\r\n'); socket.write(email); await waitForResponse(socket, '250'); console.log('Email with multiple DKIM signatures accepted'); // Send QUIT socket.write('QUIT\r\n'); await waitForResponse(socket, '221'); socket.end(); done.resolve(); } catch (err) { console.error('Test error:', err); socket.end(); done.reject(err); } }); await done.promise; }); tap.test('RFC 6376 DKIM - Various canonicalization methods', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 30000 }); socket.on('error', (err) => { console.error('Socket error:', err); done.reject(err); }); socket.on('connect', async () => { try { // Wait for greeting await waitForResponse(socket, '220'); // Send EHLO socket.write('EHLO testclient\r\n'); await waitForResponse(socket, '250'); // Send MAIL FROM socket.write('MAIL FROM:\r\n'); await waitForResponse(socket, '250'); // Send RCPT TO socket.write('RCPT TO:\r\n'); await waitForResponse(socket, '250'); // Send DATA socket.write('DATA\r\n'); await waitForResponse(socket, '354'); // Test different canonicalization methods const email = [ `From: sender@example.com`, `To: recipient@example.com`, `Subject: DKIM Canonicalization Test`, `Date: ${new Date().toUTCString()}`, `Message-ID: `, 'DKIM-Signature: v=1; a=rsa-sha256; c=simple/relaxed;', ' d=example.com; s=default;', ' h=from:to:subject;', ' bh=bodyhash=;', ' b=signature', '', 'Testing different canonicalization methods.', 'Simple header canonicalization preserves whitespace.', 'Relaxed body canonicalization normalizes whitespace.', '.', '' ].join('\r\n'); socket.write(email); await waitForResponse(socket, '250'); console.log('Email with different canonicalization accepted'); // Send QUIT socket.write('QUIT\r\n'); await waitForResponse(socket, '221'); socket.end(); done.resolve(); } catch (err) { console.error('Test error:', err); socket.end(); done.reject(err); } }); await done.promise; }); tap.test('RFC 6376 DKIM - Long header fields and folding', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 30000 }); socket.on('error', (err) => { console.error('Socket error:', err); done.reject(err); }); socket.on('connect', async () => { try { // Wait for greeting await waitForResponse(socket, '220'); // Send EHLO socket.write('EHLO testclient\r\n'); await waitForResponse(socket, '250'); // Send MAIL FROM socket.write('MAIL FROM:\r\n'); await waitForResponse(socket, '250'); // Send RCPT TO socket.write('RCPT TO:\r\n'); await waitForResponse(socket, '250'); // Send DATA socket.write('DATA\r\n'); await waitForResponse(socket, '354'); // DKIM signature with long fields that require folding const longSignature = 'b=' + 'A'.repeat(200); const email = [ `From: sender@example.com`, `To: recipient@example.com`, `Subject: DKIM Long Fields Test`, `Date: ${new Date().toUTCString()}`, `Message-ID: `, 'DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;', ' d=example.com; s=default; t=' + Math.floor(Date.now() / 1000) + ';', ' h=from:to:subject:date:message-id:content-type:mime-version;', ' bh=verylongbodyhashvalueherethatexceedsnormallength1234567890=;', ' ' + longSignature.substring(0, 70), ' ' + longSignature.substring(70, 140), ' ' + longSignature.substring(140), '', 'Testing DKIM with long header fields.', '.', '' ].join('\r\n'); socket.write(email); await waitForResponse(socket, '250'); console.log('Email with long DKIM fields accepted'); // Send QUIT socket.write('QUIT\r\n'); await waitForResponse(socket, '221'); socket.end(); done.resolve(); } catch (err) { console.error('Test error:', err); socket.end(); done.reject(err); } }); await done.promise; }); tap.test('RFC 6376 DKIM - Authentication-Results header', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 30000 }); socket.on('error', (err) => { console.error('Socket error:', err); done.reject(err); }); socket.on('connect', async () => { try { // Wait for greeting await waitForResponse(socket, '220'); // Send EHLO socket.write('EHLO testclient\r\n'); const ehloResponse = await waitForResponse(socket, '250'); // Check if server advertises DKIM support const advertisesDkim = ehloResponse.toLowerCase().includes('dkim'); console.log('Server advertises DKIM:', advertisesDkim); // Send MAIL FROM socket.write('MAIL FROM:\r\n'); await waitForResponse(socket, '250'); // Send RCPT TO socket.write('RCPT TO:\r\n'); await waitForResponse(socket, '250'); // Send DATA socket.write('DATA\r\n'); await waitForResponse(socket, '354'); // Email to test if server adds Authentication-Results header const email = [ `From: sender@example.com`, `To: recipient@example.com`, `Subject: Authentication-Results Test`, `Date: ${new Date().toUTCString()}`, `Message-ID: `, 'DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;', ' d=example.com; s=default;', ' h=from:to:subject;', ' bh=simplehash=;', ' b=simplesignature', '', 'Testing if server adds Authentication-Results header.', '.', '' ].join('\r\n'); socket.write(email); await waitForResponse(socket, '250'); console.log('Email accepted - server should process DKIM and potentially add Authentication-Results'); // Send QUIT socket.write('QUIT\r\n'); await waitForResponse(socket, '221'); socket.end(); done.resolve(); } catch (err) { console.error('Test error:', err); socket.end(); done.reject(err); } }); await done.promise; }); tap.test('cleanup - stop test server', async () => { await stopTestServer(testServer); }); export default tap.start();