import { tap, expect } from '@git.zone/tstest/tapbundle'; import * as plugins from '../plugins.js'; import * as net from 'net'; import { startTestServer, stopTestServer, TEST_PORT, sendEmailWithRawSocket } from '../server.loader.js'; import type { SmtpServer } from '../../../ts/mail/delivery/smtpserver/index.js'; let testServer: SmtpServer; tap.test('setup - start test server', async () => { testServer = await startTestServer(); await plugins.smartdelay.delayFor(1000); }); tap.test('RFC 5321 - Server greeting format', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 30000 }); socket.on('data', (data) => { const response = data.toString(); console.log('Server greeting:', response); // RFC 5321: Server must provide proper 220 greeting const greeting = response.trim(); const validGreeting = greeting.startsWith('220') && greeting.length > 10; expect(validGreeting).toBeTrue(); expect(greeting).toMatch(/^220\s+\S+/); // Should have hostname after 220 socket.write('QUIT\r\n'); socket.end(); done.resolve(); }); socket.on('error', (err) => { console.error('Socket error:', err); done.reject(err); }); await done.promise; }); tap.test('RFC 5321 - EHLO response format', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 30000 }); let dataBuffer = ''; let step = 'greeting'; socket.on('data', (data) => { dataBuffer += data.toString(); console.log('Server response:', data.toString()); if (step === 'greeting' && dataBuffer.includes('220 ')) { step = 'ehlo'; socket.write('EHLO testclient\r\n'); dataBuffer = ''; } else if (step === 'ehlo' && dataBuffer.includes('250')) { // RFC 5321: EHLO must return 250 with hostname and extensions const ehloLines = dataBuffer.split('\r\n').filter(line => line.startsWith('250')); expect(ehloLines.length).toBeGreaterThan(0); expect(ehloLines[0]).toMatch(/^250[\s-]\S+/); // First line should have hostname // Check for common extensions const extensions = ehloLines.slice(1).map(line => line.substring(4).trim()); console.log('Extensions:', extensions); socket.write('QUIT\r\n'); socket.end(); done.resolve(); } }); socket.on('error', (err) => { console.error('Socket error:', err); done.reject(err); }); await done.promise; }); tap.test('RFC 5321 - Command case insensitivity', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 30000 }); let dataBuffer = ''; let step = 'greeting'; socket.on('data', (data) => { dataBuffer += data.toString(); console.log('Server response:', data.toString()); if (step === 'greeting' && dataBuffer.includes('220 ')) { step = 'ehlo_lowercase'; // Test lowercase command socket.write('ehlo testclient\r\n'); dataBuffer = ''; } else if (step === 'ehlo_lowercase' && dataBuffer.includes('250')) { step = 'mail_mixed'; // Test mixed case command socket.write('MaIl FrOm:\r\n'); dataBuffer = ''; } else if (step === 'mail_mixed' && dataBuffer.includes('250')) { step = 'rcpt_uppercase'; // Test uppercase command socket.write('RCPT TO:\r\n'); dataBuffer = ''; } else if (step === 'rcpt_uppercase' && dataBuffer.includes('250')) { // All case variations worked console.log('All case variations accepted'); socket.write('QUIT\r\n'); socket.end(); done.resolve(); } }); socket.on('error', (err) => { console.error('Socket error:', err); done.reject(err); }); await done.promise; }); tap.test('RFC 5321 - Line length limits', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 30000 }); let dataBuffer = ''; let step = 'greeting'; socket.on('data', (data) => { dataBuffer += data.toString(); console.log('Server response:', data.toString()); if (step === 'greeting' && dataBuffer.includes('220 ')) { step = 'ehlo'; socket.write('EHLO testclient\r\n'); dataBuffer = ''; } else if (step === 'ehlo' && dataBuffer.includes('250')) { step = 'long_line'; // RFC 5321: Command line limit is 512 chars including CRLF // Test with a long MAIL FROM command (but within limit) const longDomain = 'a'.repeat(400); socket.write(`MAIL FROM:\r\n`); dataBuffer = ''; } else if (step === 'long_line') { // Should either accept (if within server limits) or reject gracefully const accepted = dataBuffer.includes('250'); const rejected = dataBuffer.includes('501') || dataBuffer.includes('500'); expect(accepted || rejected).toBeTrue(); console.log(`Long line test ${accepted ? 'accepted' : 'rejected'}`); socket.write('QUIT\r\n'); socket.end(); done.resolve(); } }); socket.on('error', (err) => { console.error('Socket error:', err); done.reject(err); }); await done.promise; }); tap.test('RFC 5321 - Standard SMTP verb compliance', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 30000 }); let dataBuffer = ''; let step = 'greeting'; const supportedVerbs: string[] = []; socket.on('data', (data) => { dataBuffer += data.toString(); console.log('Server response:', data.toString()); if (step === 'greeting' && dataBuffer.includes('220 ')) { step = 'help'; // Try HELP command to see supported verbs socket.write('HELP\r\n'); dataBuffer = ''; } else if (step === 'help') { // Parse HELP response for supported commands if (dataBuffer.includes('214') || dataBuffer.includes('502')) { // Either help text or command not implemented step = 'test_noop'; socket.write('NOOP\r\n'); dataBuffer = ''; } } else if (step === 'test_noop') { if (dataBuffer.includes('250')) { supportedVerbs.push('NOOP'); } step = 'test_rset'; socket.write('RSET\r\n'); dataBuffer = ''; } else if (step === 'test_rset') { if (dataBuffer.includes('250')) { supportedVerbs.push('RSET'); } step = 'test_vrfy'; socket.write('VRFY test@example.com\r\n'); dataBuffer = ''; } else if (step === 'test_vrfy') { // VRFY may be disabled for security (252 or 502) if (dataBuffer.includes('250') || dataBuffer.includes('252')) { supportedVerbs.push('VRFY'); } // Check minimum required verbs const requiredVerbs = ['NOOP', 'RSET']; const hasRequired = requiredVerbs.every(verb => supportedVerbs.includes(verb) || verb === 'VRFY' // VRFY is optional ); console.log('Supported verbs:', supportedVerbs); expect(hasRequired).toBeTrue(); socket.write('QUIT\r\n'); socket.end(); done.resolve(); } }); socket.on('error', (err) => { console.error('Socket error:', err); done.reject(err); }); await done.promise; }); tap.test('RFC 5321 - Required minimum extensions', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 30000 }); let dataBuffer = ''; socket.on('data', (data) => { dataBuffer += data.toString(); console.log('Server response:', data.toString()); if (dataBuffer.includes('220 ')) { socket.write('EHLO testclient\r\n'); dataBuffer = ''; } else if (dataBuffer.includes('250')) { // Check for extensions const lines = dataBuffer.split('\r\n'); const extensions = lines .filter(line => line.startsWith('250-') || (line.startsWith('250 ') && lines.indexOf(line) > 0)) .map(line => line.substring(4).split(' ')[0].toUpperCase()); console.log('Server extensions:', extensions); // RFC 5321 recommends these extensions const recommendedExtensions = ['8BITMIME', 'SIZE', 'PIPELINING']; const hasRecommended = recommendedExtensions.filter(ext => extensions.includes(ext)); console.log('Recommended extensions present:', hasRecommended); socket.write('QUIT\r\n'); socket.end(); done.resolve(); } }); socket.on('error', (err) => { console.error('Socket error:', err); done.reject(err); }); await done.promise; }); tap.test('cleanup - stop test server', async () => { await stopTestServer(testServer); }); tap.start();