import { tap, expect } from '@git.zone/tstest/tapbundle'; import { UnifiedEmailServer } from '../ts/mail/routing/classes.unified.email.server.js'; import { RustSecurityBridge } from '../ts/security/classes.rustsecuritybridge.js'; import { Email } from '../ts/mail/core/classes.email.js'; import * as net from 'net'; import * as dns from 'dns'; const storageMap = new Map(); const mockDcRouter = { storageManager: { get: async (key: string) => storageMap.get(key) || null, set: async (key: string, value: string) => { storageMap.set(key, value); }, }, }; let server: UnifiedEmailServer; let bridgeAvailable = false; let mockSmtpServer: net.Server; /** * Create a minimal mock SMTP server that accepts any email. */ function createMockSmtpServer(port: number): Promise { return new Promise((resolve, reject) => { const srv = net.createServer((socket) => { socket.write('220 mock-smtp.local ESMTP MockServer\r\n'); let inData = false; let dataBuffer = ''; socket.on('data', (chunk) => { const input = chunk.toString(); if (inData) { dataBuffer += input; if (dataBuffer.includes('\r\n.\r\n')) { inData = false; dataBuffer = ''; socket.write('250 2.0.0 Ok: queued\r\n'); } return; } const lines = input.split('\r\n').filter((l: string) => l.length > 0); for (const line of lines) { const cmd = line.toUpperCase(); if (cmd.startsWith('EHLO') || cmd.startsWith('HELO')) { socket.write(`250-mock-smtp.local\r\n250-SIZE 10485760\r\n250 OK\r\n`); } else if (cmd.startsWith('MAIL FROM')) { socket.write('250 2.1.0 Ok\r\n'); } else if (cmd.startsWith('RCPT TO')) { socket.write('250 2.1.5 Ok\r\n'); } else if (cmd === 'DATA') { inData = true; dataBuffer = ''; socket.write('354 End data with .\r\n'); } else if (cmd === 'QUIT') { socket.write('221 2.0.0 Bye\r\n'); socket.end(); } else if (cmd === 'RSET') { socket.write('250 2.0.0 Ok\r\n'); } } }); }); srv.listen(port, '127.0.0.1', () => { resolve(srv); }); srv.on('error', reject); }); } // Store original resolveMx so we can restore it const originalResolveMx = dns.promises.Resolver.prototype.resolveMx; tap.test('setup - start server and mock SMTP', async () => { RustSecurityBridge.resetInstance(); server = new UnifiedEmailServer(mockDcRouter, { ports: [10425], hostname: 'test.mta.local', domains: [ { domain: 'mta-test.com', dnsMode: 'forward' }, ], routes: [ { name: 'mta-route', priority: 10, match: { recipients: '*@mta-test.com' }, action: { type: 'deliver' }, }, { name: 'process-route', priority: 20, match: { recipients: '*@process-test.com' }, action: { type: 'process', options: { contentScanning: true, scanners: [{ type: 'spam' }], }, }, }, ], }); try { await server.start(); bridgeAvailable = true; } catch (err) { console.log(`SKIP: Server failed to start — ${(err as Error).message}`); console.log('Build the Rust binary with: cd rust && cargo build --release'); return; } mockSmtpServer = await createMockSmtpServer(10426); }); tap.test('MX resolution for a public domain', async () => { if (!bridgeAvailable) { console.log('SKIP'); return; } // Use the delivery system's resolveMxForDomain via a quick DNS lookup const resolver = new dns.promises.Resolver(); try { const records = await resolver.resolveMx('gmail.com'); expect(records).toBeTruthy(); expect(records.length).toBeGreaterThan(0); // Each record should have exchange and priority for (const rec of records) { expect(typeof rec.exchange).toEqual('string'); expect(typeof rec.priority).toEqual('number'); } console.log(`Resolved ${records.length} MX records for gmail.com`); } catch (err) { console.log(`SKIP: DNS resolution failed (network may be unavailable): ${(err as Error).message}`); } }); tap.test('group recipients by domain', async () => { if (!bridgeAvailable) { console.log('SKIP'); return; } // Test the grouping logic directly const recipients = [ 'alice@example.com', 'bob@example.com', 'carol@other.org', 'dave@example.com', 'eve@other.org', ]; const groups = new Map(); for (const rcpt of recipients) { const domain = rcpt.split('@')[1]?.toLowerCase(); if (!domain) continue; const list = groups.get(domain) || []; list.push(rcpt); groups.set(domain, list); } expect(groups.size).toEqual(2); expect(groups.get('example.com')!.length).toEqual(3); expect(groups.get('other.org')!.length).toEqual(2); }); tap.test('MTA delivery to mock SMTP server via mocked MX', async () => { if (!bridgeAvailable) { console.log('SKIP'); return; } // Mock dns.promises.Resolver.resolveMx to return 127.0.0.1 dns.promises.Resolver.prototype.resolveMx = async function (_hostname: string) { return [{ exchange: '127.0.0.1', priority: 10 }]; }; const email = new Email({ from: 'sender@mta-test.com', to: 'recipient@target-domain.com', subject: 'MTA Delivery Test', text: 'Testing MTA delivery with MX resolution.', }); // Use sendOutboundEmail at the resolved MX host (which is mocked to 127.0.0.1) // But the real test is the delivery system's handleMtaDelivery, which we test // by sending through the server's outbound path with the mock MX. // Direct test: resolve MX then send const resolver = new dns.promises.Resolver(); const mxRecords = await resolver.resolveMx('target-domain.com'); expect(mxRecords[0].exchange).toEqual('127.0.0.1'); // Send via the resolved MX host to the mock SMTP server on port 10425 // Note: MTA delivery uses port 25 by default, but our mock is on 10425. // We test the sendOutboundEmail path directly with the mock MX host. const result = await server.sendOutboundEmail('127.0.0.1', 10426, email); expect(result).toBeTruthy(); expect(result.accepted.length).toBeGreaterThan(0); expect(result.response).toInclude('2.0.0'); // Restore original resolveMx dns.promises.Resolver.prototype.resolveMx = originalResolveMx; }); tap.test('MTA delivery - connection refused to unreachable MX', async () => { if (!bridgeAvailable) { console.log('SKIP'); return; } const email = new Email({ from: 'sender@mta-test.com', to: 'recipient@unreachable-domain.com', subject: 'Connection Refused MX Test', text: 'This should fail — no server at the target.', }); // Send to a port that nothing is listening on try { await server.sendOutboundEmail('127.0.0.1', 59789, email); throw new Error('Expected sendOutboundEmail to fail'); } catch (err: any) { expect(err).toBeTruthy(); expect(err.message.length).toBeGreaterThan(0); console.log(`Got expected error: ${err.message}`); } }); tap.test('MTA delivery with multiple recipients across domains', async () => { if (!bridgeAvailable) { console.log('SKIP'); return; } // Mock MX to return 127.0.0.1 for all domains dns.promises.Resolver.prototype.resolveMx = async function (_hostname: string) { return [{ exchange: '127.0.0.1', priority: 10 }]; }; const email = new Email({ from: 'sender@mta-test.com', to: ['alice@domain-a.com', 'bob@domain-b.com'], subject: 'Multi-Domain MTA Test', text: 'Testing delivery to multiple domains.', }); // Send to each recipient's domain individually (simulating MTA behavior) for (const recipient of email.to) { const singleEmail = new Email({ from: email.from, to: recipient, subject: email.subject, text: email.text, }); const result = await server.sendOutboundEmail('127.0.0.1', 10426, singleEmail); expect(result.accepted.length).toEqual(1); } // Restore original resolveMx dns.promises.Resolver.prototype.resolveMx = originalResolveMx; }); tap.test('E2E: send real email to hello@task.vc via MX resolution', async () => { if (!bridgeAvailable) { console.log('SKIP'); return; } // Resolve real MX records for task.vc const resolver = new dns.promises.Resolver(); const mxRecords = await resolver.resolveMx('task.vc'); expect(mxRecords.length).toBeGreaterThan(0); const mxHost = mxRecords.sort((a, b) => a.priority - b.priority)[0].exchange; console.log(`Resolved MX for task.vc: ${mxHost} (priority ${mxRecords[0].priority})`); const email = new Email({ from: 'test@mta-test.com', to: 'hello@task.vc', subject: `MTA E2E Test — ${new Date().toISOString()}`, text: 'This is an automated E2E test from @serve.zone/mailer verifying real MX resolution and outbound SMTP delivery.', }); const result = await server.sendOutboundEmail(mxHost, 25, email); expect(result).toBeTruthy(); expect(result.accepted).toBeTruthy(); expect(result.accepted.length).toEqual(1); expect(result.accepted[0]).toEqual('hello@task.vc'); expect(result.response).toInclude('2.0.0'); console.log(`Email delivered to hello@task.vc via ${mxHost}: ${result.response}`); }); tap.test('cleanup - stop server and mock SMTP', async () => { // Restore MX resolver in case it wasn't restored dns.promises.Resolver.prototype.resolveMx = originalResolveMx; // Force-close mock server (destroy all open sockets) if (mockSmtpServer) { mockSmtpServer.close(); } if (bridgeAvailable) { await server.stop(); } await tap.stopForcefully(); }); export default tap.start();