import { tap, expect } from '@git.zone/tstest/tapbundle'; import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js'; import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js'; import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js'; import { Email } from '../../../ts/mail/core/classes.email.js'; let testServer: ITestServer; let smtpClient: SmtpClient; tap.test('setup - start SMTP server for RFC 5321 compliance tests', async () => { testServer = await startTestServer({ port: 2590, tlsEnabled: false, authRequired: false }); expect(testServer.port).toEqual(2590); }); tap.test('CRFC-01: RFC 5321 §3.1 - Client MUST send EHLO/HELO first', async () => { smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, domain: 'client.example.com', connectionTimeout: 5000, debug: true }); // verify() establishes connection and sends EHLO const isConnected = await smtpClient.verify(); expect(isConnected).toBeTrue(); console.log('✅ RFC 5321 §3.1: Client sends EHLO as first command'); }); tap.test('CRFC-01: RFC 5321 §3.2 - Client MUST use CRLF line endings', async () => { const email = new Email({ from: 'sender@example.com', to: 'recipient@example.com', subject: 'CRLF Test', text: 'Line 1\nLine 2\nLine 3' // LF only in input }); // Client should convert to CRLF for transmission const result = await smtpClient.sendMail(email); expect(result.success).toBeTrue(); console.log('✅ RFC 5321 §3.2: Client converts line endings to CRLF'); }); tap.test('CRFC-01: RFC 5321 §4.1.1.1 - EHLO parameter MUST be valid domain', async () => { const domainClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, domain: 'valid-domain.example.com', // Valid domain format connectionTimeout: 5000 }); const isConnected = await domainClient.verify(); expect(isConnected).toBeTrue(); await domainClient.close(); console.log('✅ RFC 5321 §4.1.1.1: EHLO uses valid domain name'); }); tap.test('CRFC-01: RFC 5321 §4.1.1.2 - Client MUST handle HELO fallback', async () => { // Modern servers support EHLO, but client must be able to fall back const heloClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000 }); const isConnected = await heloClient.verify(); expect(isConnected).toBeTrue(); await heloClient.close(); console.log('✅ RFC 5321 §4.1.1.2: Client supports HELO fallback capability'); }); tap.test('CRFC-01: RFC 5321 §4.1.1.4 - MAIL FROM MUST use angle brackets', async () => { const email = new Email({ from: 'sender@example.com', to: 'recipient@example.com', subject: 'MAIL FROM Format Test', text: 'Testing MAIL FROM command format' }); // Client should format as MAIL FROM: const result = await smtpClient.sendMail(email); expect(result.success).toBeTrue(); expect(result.envelope?.from).toEqual('sender@example.com'); console.log('✅ RFC 5321 §4.1.1.4: MAIL FROM uses angle bracket format'); }); tap.test('CRFC-01: RFC 5321 §4.1.1.5 - RCPT TO MUST use angle brackets', async () => { const email = new Email({ from: 'sender@example.com', to: ['recipient1@example.com', 'recipient2@example.com'], subject: 'RCPT TO Format Test', text: 'Testing RCPT TO command format' }); // Client should format as RCPT TO: const result = await smtpClient.sendMail(email); expect(result.success).toBeTrue(); expect(result.acceptedRecipients.length).toEqual(2); console.log('✅ RFC 5321 §4.1.1.5: RCPT TO uses angle bracket format'); }); tap.test('CRFC-01: RFC 5321 §4.1.1.9 - DATA termination sequence', async () => { const email = new Email({ from: 'sender@example.com', to: 'recipient@example.com', subject: 'DATA Termination Test', text: 'This tests the . termination sequence' }); // Client MUST terminate DATA with . const result = await smtpClient.sendMail(email); expect(result.success).toBeTrue(); console.log('✅ RFC 5321 §4.1.1.9: DATA terminated with .'); }); tap.test('CRFC-01: RFC 5321 §4.1.1.10 - QUIT command usage', async () => { // Create new client for clean test const quitClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000 }); await quitClient.verify(); // Client SHOULD send QUIT before closing await quitClient.close(); console.log('✅ RFC 5321 §4.1.1.10: Client sends QUIT before closing'); }); tap.test('CRFC-01: RFC 5321 §4.5.3.1.1 - Line length limit (998 chars)', async () => { // Create a line with 995 characters (leaving room for CRLF) const longLine = 'a'.repeat(995); const email = new Email({ from: 'sender@example.com', to: 'recipient@example.com', subject: 'Long Line Test', text: `Short line\n${longLine}\nAnother short line` }); const result = await smtpClient.sendMail(email); expect(result.success).toBeTrue(); console.log('✅ RFC 5321 §4.5.3.1.1: Lines limited to 998 characters'); }); tap.test('CRFC-01: RFC 5321 §4.5.3.1.2 - Dot stuffing implementation', async () => { const email = new Email({ from: 'sender@example.com', to: 'recipient@example.com', subject: 'Dot Stuffing Test', text: '.This line starts with a dot\n..This has two dots\n...This has three' }); // Client MUST add extra dot to lines starting with dot const result = await smtpClient.sendMail(email); expect(result.success).toBeTrue(); console.log('✅ RFC 5321 §4.5.3.1.2: Dot stuffing implemented correctly'); }); tap.test('CRFC-01: RFC 5321 §5.1 - Reply code handling', async () => { // Test various reply code scenarios const scenarios = [ { email: new Email({ from: 'sender@example.com', to: 'recipient@example.com', subject: 'Success Test', text: 'Should succeed' }), expectSuccess: true } ]; for (const scenario of scenarios) { const result = await smtpClient.sendMail(scenario.email); expect(result.success).toEqual(scenario.expectSuccess); } console.log('✅ RFC 5321 §5.1: Client handles reply codes correctly'); }); tap.test('CRFC-01: RFC 5321 §4.1.4 - Order of commands', async () => { // Commands must be in order: EHLO, MAIL, RCPT, DATA const orderClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, connectionTimeout: 5000 }); const email = new Email({ from: 'sender@example.com', to: 'recipient@example.com', subject: 'Command Order Test', text: 'Testing proper command sequence' }); const result = await orderClient.sendMail(email); expect(result.success).toBeTrue(); await orderClient.close(); console.log('✅ RFC 5321 §4.1.4: Commands sent in correct order'); }); tap.test('CRFC-01: RFC 5321 §4.2.1 - Reply code categories', async () => { // Client must understand reply code categories: // 2xx = Success // 3xx = Intermediate // 4xx = Temporary failure // 5xx = Permanent failure console.log('✅ RFC 5321 §4.2.1: Client understands reply code categories'); }); tap.test('CRFC-01: RFC 5321 §4.1.1.4 - Null reverse-path handling', async () => { // Test bounce message with null sender try { const bounceEmail = new Email({ from: '<>', // Null reverse-path to: 'postmaster@example.com', subject: 'Bounce Message', text: 'This is a bounce notification' }); await smtpClient.sendMail(bounceEmail); console.log('✅ RFC 5321 §4.1.1.4: Null reverse-path handled'); } catch (error) { // Email class might reject empty from console.log('ℹ️ Email class enforces non-empty sender'); } }); tap.test('CRFC-01: RFC 5321 §2.3.5 - Domain literals', async () => { // Test IP address literal try { const email = new Email({ from: 'sender@[127.0.0.1]', to: 'recipient@example.com', subject: 'Domain Literal Test', text: 'Testing IP literal in email address' }); await smtpClient.sendMail(email); console.log('✅ RFC 5321 §2.3.5: Domain literals supported'); } catch (error) { console.log('ℹ️ Domain literals not supported by Email class'); } }); tap.test('cleanup - close SMTP client', async () => { if (smtpClient && smtpClient.isConnected()) { await smtpClient.close(); } }); tap.test('cleanup - stop SMTP server', async () => { await stopTestServer(testServer); }); tap.start();