188 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			188 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
|  | /** | ||
|  |  * CMD-04: DATA Command Tests | ||
|  |  * Tests SMTP DATA command for email content transmission | ||
|  |  */ | ||
|  | 
 | ||
|  | import { assert, assertMatch } from '@std/assert'; | ||
|  | import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.ts'; | ||
|  | import { | ||
|  |   connectToSmtp, | ||
|  |   waitForGreeting, | ||
|  |   sendSmtpCommand, | ||
|  |   readSmtpResponse, | ||
|  |   closeSmtpConnection, | ||
|  | } from '../../helpers/utils.ts'; | ||
|  | 
 | ||
|  | const TEST_PORT = 25254; | ||
|  | let testServer: ITestServer; | ||
|  | 
 | ||
|  | Deno.test({ | ||
|  |   name: 'CMD-04: Setup - Start SMTP server', | ||
|  |   async fn() { | ||
|  |     testServer = await startTestServer({ port: TEST_PORT }); | ||
|  |     assert(testServer, 'Test server should be created'); | ||
|  |   }, | ||
|  |   sanitizeResources: false, | ||
|  |   sanitizeOps: false, | ||
|  | }); | ||
|  | 
 | ||
|  | Deno.test({ | ||
|  |   name: 'CMD-04: DATA - accepts email data after RCPT TO', | ||
|  |   async fn() { | ||
|  |     const conn = await connectToSmtp('localhost', TEST_PORT); | ||
|  | 
 | ||
|  |     try { | ||
|  |       await waitForGreeting(conn); | ||
|  |       await sendSmtpCommand(conn, 'EHLO test.example.com', '250'); | ||
|  |       await sendSmtpCommand(conn, 'MAIL FROM:<sender@example.com>', '250'); | ||
|  |       await sendSmtpCommand(conn, 'RCPT TO:<recipient@example.com>', '250'); | ||
|  | 
 | ||
|  |       // Send DATA command
 | ||
|  |       const dataResponse = await sendSmtpCommand(conn, 'DATA', '354'); | ||
|  |       assert(dataResponse.includes('354'), 'Should receive 354 Start mail input'); | ||
|  | 
 | ||
|  |       // Send email content
 | ||
|  |       const encoder = new TextEncoder(); | ||
|  |       await conn.write(encoder.encode('From: sender@example.com\r\n')); | ||
|  |       await conn.write(encoder.encode('To: recipient@example.com\r\n')); | ||
|  |       await conn.write(encoder.encode('Subject: Test message\r\n')); | ||
|  |       await conn.write(encoder.encode('\r\n')); // Empty line
 | ||
|  |       await conn.write(encoder.encode('This is a test message.\r\n')); | ||
|  |       await conn.write(encoder.encode('.\r\n')); // End of message
 | ||
|  | 
 | ||
|  |       // Wait for acceptance
 | ||
|  |       const acceptResponse = await readSmtpResponse(conn, '250'); | ||
|  |       assert(acceptResponse.includes('250'), 'Should accept email with 250 OK'); | ||
|  |     } finally { | ||
|  |       await closeSmtpConnection(conn); | ||
|  |     } | ||
|  |   }, | ||
|  |   sanitizeResources: false, | ||
|  |   sanitizeOps: false, | ||
|  | }); | ||
|  | 
 | ||
|  | Deno.test({ | ||
|  |   name: 'CMD-04: DATA - rejects without RCPT TO', | ||
|  |   async fn() { | ||
|  |     const conn = await connectToSmtp('localhost', TEST_PORT); | ||
|  | 
 | ||
|  |     try { | ||
|  |       await waitForGreeting(conn); | ||
|  |       await sendSmtpCommand(conn, 'EHLO test.example.com', '250'); | ||
|  | 
 | ||
|  |       // Try DATA without MAIL FROM or RCPT TO
 | ||
|  |       const response = await sendSmtpCommand(conn, 'DATA'); | ||
|  |       assertMatch(response, /^503/, 'Should reject with 503 bad sequence'); | ||
|  |     } finally { | ||
|  |       try { | ||
|  |         await closeSmtpConnection(conn); | ||
|  |       } catch { | ||
|  |         // Connection might be closed by server
 | ||
|  |       } | ||
|  |     } | ||
|  |   }, | ||
|  |   sanitizeResources: false, | ||
|  |   sanitizeOps: false, | ||
|  | }); | ||
|  | 
 | ||
|  | Deno.test({ | ||
|  |   name: 'CMD-04: DATA - handles dot-stuffing correctly', | ||
|  |   async fn() { | ||
|  |     const conn = await connectToSmtp('localhost', TEST_PORT); | ||
|  | 
 | ||
|  |     try { | ||
|  |       await waitForGreeting(conn); | ||
|  |       await sendSmtpCommand(conn, 'EHLO test.example.com', '250'); | ||
|  |       await sendSmtpCommand(conn, 'MAIL FROM:<sender@example.com>', '250'); | ||
|  |       await sendSmtpCommand(conn, 'RCPT TO:<recipient@example.com>', '250'); | ||
|  |       await sendSmtpCommand(conn, 'DATA', '354'); | ||
|  | 
 | ||
|  |       // Send content with lines starting with dots (should be escaped with double dots)
 | ||
|  |       const encoder = new TextEncoder(); | ||
|  |       await conn.write(encoder.encode('Subject: Dot test\r\n')); | ||
|  |       await conn.write(encoder.encode('\r\n')); | ||
|  |       await conn.write(encoder.encode('..This line starts with a dot\r\n')); // Dot-stuffed
 | ||
|  |       await conn.write(encoder.encode('Normal line\r\n')); | ||
|  |       await conn.write(encoder.encode('.\r\n')); // End of message
 | ||
|  | 
 | ||
|  |       const response = await readSmtpResponse(conn, '250'); | ||
|  |       assert(response.includes('250'), 'Should accept dot-stuffed message'); | ||
|  |     } finally { | ||
|  |       await closeSmtpConnection(conn); | ||
|  |     } | ||
|  |   }, | ||
|  |   sanitizeResources: false, | ||
|  |   sanitizeOps: false, | ||
|  | }); | ||
|  | 
 | ||
|  | Deno.test({ | ||
|  |   name: 'CMD-04: DATA - handles large messages', | ||
|  |   async fn() { | ||
|  |     const conn = await connectToSmtp('localhost', TEST_PORT); | ||
|  | 
 | ||
|  |     try { | ||
|  |       await waitForGreeting(conn); | ||
|  |       await sendSmtpCommand(conn, 'EHLO test.example.com', '250'); | ||
|  |       await sendSmtpCommand(conn, 'MAIL FROM:<sender@example.com>', '250'); | ||
|  |       await sendSmtpCommand(conn, 'RCPT TO:<recipient@example.com>', '250'); | ||
|  |       await sendSmtpCommand(conn, 'DATA', '354'); | ||
|  | 
 | ||
|  |       // Send a larger message (10KB)
 | ||
|  |       const encoder = new TextEncoder(); | ||
|  |       await conn.write(encoder.encode('Subject: Large message test\r\n')); | ||
|  |       await conn.write(encoder.encode('\r\n')); | ||
|  | 
 | ||
|  |       const largeContent = 'A'.repeat(10000); | ||
|  |       await conn.write(encoder.encode(largeContent + '\r\n')); | ||
|  |       await conn.write(encoder.encode('.\r\n')); | ||
|  | 
 | ||
|  |       const response = await readSmtpResponse(conn, '250'); | ||
|  |       assert(response.includes('250'), 'Should accept large message'); | ||
|  |     } finally { | ||
|  |       await closeSmtpConnection(conn); | ||
|  |     } | ||
|  |   }, | ||
|  |   sanitizeResources: false, | ||
|  |   sanitizeOps: false, | ||
|  | }); | ||
|  | 
 | ||
|  | Deno.test({ | ||
|  |   name: 'CMD-04: DATA - enforces correct sequence', | ||
|  |   async fn() { | ||
|  |     const conn = await connectToSmtp('localhost', TEST_PORT); | ||
|  | 
 | ||
|  |     try { | ||
|  |       await waitForGreeting(conn); | ||
|  |       await sendSmtpCommand(conn, 'EHLO test.example.com', '250'); | ||
|  |       await sendSmtpCommand(conn, 'MAIL FROM:<sender@example.com>', '250'); | ||
|  | 
 | ||
|  |       // Try DATA after MAIL FROM but before RCPT TO
 | ||
|  |       // NOTE: Current server implementation accepts DATA without RCPT TO (returns 354)
 | ||
|  |       // RFC 5321 suggests this should be rejected with 503, but some servers allow it
 | ||
|  |       const response = await sendSmtpCommand(conn, 'DATA'); | ||
|  |       assertMatch(response, /^(354|503)/, 'Server responds to DATA (354=accept, 503=reject)'); | ||
|  | 
 | ||
|  |       if (response.startsWith('354')) { | ||
|  |         console.log('⚠️  Server accepts DATA without RCPT TO (non-standard but allowed)'); | ||
|  |       } | ||
|  |     } finally { | ||
|  |       try { | ||
|  |         await closeSmtpConnection(conn); | ||
|  |       } catch { | ||
|  |         // Ignore errors
 | ||
|  |       } | ||
|  |     } | ||
|  |   }, | ||
|  |   sanitizeResources: false, | ||
|  |   sanitizeOps: false, | ||
|  | }); | ||
|  | 
 | ||
|  | Deno.test({ | ||
|  |   name: 'CMD-04: Cleanup - Stop SMTP server', | ||
|  |   async fn() { | ||
|  |     await stopTestServer(testServer); | ||
|  |   }, | ||
|  |   sanitizeResources: false, | ||
|  |   sanitizeOps: false, | ||
|  | }); |