246 lines
		
	
	
		
			9.0 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			246 lines
		
	
	
		
			9.0 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
|  | /** | ||
|  |  * EP-01: Basic Email Sending Tests | ||
|  |  * Tests complete email sending lifecycle through SMTP server | ||
|  |  */ | ||
|  | 
 | ||
|  | import { assert, assertEquals } 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 = 25258; | ||
|  | let testServer: ITestServer; | ||
|  | 
 | ||
|  | Deno.test({ | ||
|  |   name: 'EP-01: 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: 'EP-01: Basic Email - complete SMTP transaction flow', | ||
|  |   async fn() { | ||
|  |     const conn = await connectToSmtp('localhost', TEST_PORT); | ||
|  |     const fromAddress = 'sender@example.com'; | ||
|  |     const toAddress = 'recipient@example.com'; | ||
|  |     const emailContent = `Subject: Production Test Email\r\nFrom: ${fromAddress}\r\nTo: ${toAddress}\r\nDate: ${new Date().toUTCString()}\r\n\r\nThis is a test email sent during production testing.\r\nTest ID: EP-01\r\nTimestamp: ${Date.now()}\r\n`; | ||
|  | 
 | ||
|  |     try { | ||
|  |       // Step 1: CONNECT - Wait for greeting
 | ||
|  |       const greeting = await waitForGreeting(conn); | ||
|  |       assert(greeting.includes('220'), 'Should receive 220 greeting'); | ||
|  | 
 | ||
|  |       // Step 2: EHLO
 | ||
|  |       const ehloResponse = await sendSmtpCommand(conn, 'EHLO test.example.com', '250'); | ||
|  |       assert(ehloResponse.includes('250'), 'Should accept EHLO'); | ||
|  | 
 | ||
|  |       // Step 3: MAIL FROM
 | ||
|  |       const mailFromResponse = await sendSmtpCommand(conn, `MAIL FROM:<${fromAddress}>`, '250'); | ||
|  |       assert(mailFromResponse.includes('250'), 'Should accept MAIL FROM'); | ||
|  | 
 | ||
|  |       // Step 4: RCPT TO
 | ||
|  |       const rcptToResponse = await sendSmtpCommand(conn, `RCPT TO:<${toAddress}>`, '250'); | ||
|  |       assert(rcptToResponse.includes('250'), 'Should accept RCPT TO'); | ||
|  | 
 | ||
|  |       // Step 5: DATA
 | ||
|  |       const dataResponse = await sendSmtpCommand(conn, 'DATA', '354'); | ||
|  |       assert(dataResponse.includes('354'), 'Should accept DATA command'); | ||
|  | 
 | ||
|  |       // Step 6: EMAIL CONTENT
 | ||
|  |       const encoder = new TextEncoder(); | ||
|  |       await conn.write(encoder.encode(emailContent)); | ||
|  |       await conn.write(encoder.encode('.\r\n')); // End of data marker
 | ||
|  | 
 | ||
|  |       const contentResponse = await readSmtpResponse(conn, '250'); | ||
|  |       assert(contentResponse.includes('250'), 'Should accept email content'); | ||
|  | 
 | ||
|  |       // Step 7: QUIT
 | ||
|  |       const quitResponse = await sendSmtpCommand(conn, 'QUIT', '221'); | ||
|  |       assert(quitResponse.includes('221'), 'Should respond to QUIT'); | ||
|  | 
 | ||
|  |       console.log('✓ Complete email sending flow: CONNECT → EHLO → MAIL FROM → RCPT TO → DATA → CONTENT → QUIT'); | ||
|  |     } finally { | ||
|  |       try { | ||
|  |         conn.close(); | ||
|  |       } catch { | ||
|  |         // Connection may already be closed
 | ||
|  |       } | ||
|  |     } | ||
|  |   }, | ||
|  |   sanitizeResources: false, | ||
|  |   sanitizeOps: false, | ||
|  | }); | ||
|  | 
 | ||
|  | Deno.test({ | ||
|  |   name: 'EP-01: Basic Email - send email with MIME attachment', | ||
|  |   async fn() { | ||
|  |     const conn = await connectToSmtp('localhost', TEST_PORT); | ||
|  |     const fromAddress = 'sender@example.com'; | ||
|  |     const toAddress = 'recipient@example.com'; | ||
|  |     const boundary = '----=_Part_0_1234567890'; | ||
|  | 
 | ||
|  |     const emailContent = `Subject: Email with Attachment\r\nFrom: ${fromAddress}\r\nTo: ${toAddress}\r\nMIME-Version: 1.0\r\nContent-Type: multipart/mixed; boundary="${boundary}"\r\n\r\n--${boundary}\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\nThis email contains an attachment.\r\n\r\n--${boundary}\r\nContent-Type: text/plain; name="test.txt"\r\nContent-Disposition: attachment; filename="test.txt"\r\nContent-Transfer-Encoding: base64\r\n\r\nVGhpcyBpcyBhIHRlc3QgZmlsZS4=\r\n\r\n--${boundary}--\r\n`; | ||
|  | 
 | ||
|  |     try { | ||
|  |       await waitForGreeting(conn); | ||
|  |       await sendSmtpCommand(conn, 'EHLO test.example.com', '250'); | ||
|  |       await sendSmtpCommand(conn, `MAIL FROM:<${fromAddress}>`, '250'); | ||
|  |       await sendSmtpCommand(conn, `RCPT TO:<${toAddress}>`, '250'); | ||
|  |       await sendSmtpCommand(conn, 'DATA', '354'); | ||
|  | 
 | ||
|  |       // Send MIME email content
 | ||
|  |       const encoder = new TextEncoder(); | ||
|  |       await conn.write(encoder.encode(emailContent)); | ||
|  |       await conn.write(encoder.encode('.\r\n')); | ||
|  | 
 | ||
|  |       const response = await readSmtpResponse(conn, '250'); | ||
|  |       assert(response.includes('250'), 'Should accept MIME email with attachment'); | ||
|  | 
 | ||
|  |       await sendSmtpCommand(conn, 'QUIT', '221'); | ||
|  |       console.log('✓ Successfully sent email with MIME attachment'); | ||
|  |     } finally { | ||
|  |       try { | ||
|  |         conn.close(); | ||
|  |       } catch { | ||
|  |         // Ignore
 | ||
|  |       } | ||
|  |     } | ||
|  |   }, | ||
|  |   sanitizeResources: false, | ||
|  |   sanitizeOps: false, | ||
|  | }); | ||
|  | 
 | ||
|  | Deno.test({ | ||
|  |   name: 'EP-01: Basic Email - send HTML email', | ||
|  |   async fn() { | ||
|  |     const conn = await connectToSmtp('localhost', TEST_PORT); | ||
|  |     const fromAddress = 'sender@example.com'; | ||
|  |     const toAddress = 'recipient@example.com'; | ||
|  |     const boundary = '----=_Part_0_987654321'; | ||
|  | 
 | ||
|  |     const emailContent = `Subject: HTML Email Test\r\nFrom: ${fromAddress}\r\nTo: ${toAddress}\r\nMIME-Version: 1.0\r\nContent-Type: multipart/alternative; boundary="${boundary}"\r\n\r\n--${boundary}\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\nThis is the plain text version.\r\n\r\n--${boundary}\r\nContent-Type: text/html; charset=UTF-8\r\n\r\n<html><body><h1>HTML Email</h1><p>This is the <strong>HTML</strong> version.</p></body></html>\r\n\r\n--${boundary}--\r\n`; | ||
|  | 
 | ||
|  |     try { | ||
|  |       await waitForGreeting(conn); | ||
|  |       await sendSmtpCommand(conn, 'EHLO test.example.com', '250'); | ||
|  |       await sendSmtpCommand(conn, `MAIL FROM:<${fromAddress}>`, '250'); | ||
|  |       await sendSmtpCommand(conn, `RCPT TO:<${toAddress}>`, '250'); | ||
|  |       await sendSmtpCommand(conn, 'DATA', '354'); | ||
|  | 
 | ||
|  |       // Send HTML email content
 | ||
|  |       const encoder = new TextEncoder(); | ||
|  |       await conn.write(encoder.encode(emailContent)); | ||
|  |       await conn.write(encoder.encode('.\r\n')); | ||
|  | 
 | ||
|  |       const response = await readSmtpResponse(conn, '250'); | ||
|  |       assert(response.includes('250'), 'Should accept HTML email'); | ||
|  | 
 | ||
|  |       await sendSmtpCommand(conn, 'QUIT', '221'); | ||
|  |       console.log('✓ Successfully sent HTML email (multipart/alternative)'); | ||
|  |     } finally { | ||
|  |       try { | ||
|  |         conn.close(); | ||
|  |       } catch { | ||
|  |         // Ignore
 | ||
|  |       } | ||
|  |     } | ||
|  |   }, | ||
|  |   sanitizeResources: false, | ||
|  |   sanitizeOps: false, | ||
|  | }); | ||
|  | 
 | ||
|  | Deno.test({ | ||
|  |   name: 'EP-01: Basic Email - send email with custom headers', | ||
|  |   async fn() { | ||
|  |     const conn = await connectToSmtp('localhost', TEST_PORT); | ||
|  |     const fromAddress = 'sender@example.com'; | ||
|  |     const toAddress = 'recipient@example.com'; | ||
|  | 
 | ||
|  |     const emailContent = `Subject: Custom Headers Test\r\nFrom: ${fromAddress}\r\nTo: ${toAddress}\r\nX-Custom-Header: CustomValue\r\nX-Priority: 1\r\nX-Mailer: SMTP Test Suite\r\nReply-To: noreply@example.com\r\nOrganization: Test Organization\r\n\r\nThis email contains custom headers.\r\n`; | ||
|  | 
 | ||
|  |     try { | ||
|  |       await waitForGreeting(conn); | ||
|  |       await sendSmtpCommand(conn, 'EHLO test.example.com', '250'); | ||
|  |       await sendSmtpCommand(conn, `MAIL FROM:<${fromAddress}>`, '250'); | ||
|  |       await sendSmtpCommand(conn, `RCPT TO:<${toAddress}>`, '250'); | ||
|  |       await sendSmtpCommand(conn, 'DATA', '354'); | ||
|  | 
 | ||
|  |       // Send email with custom headers
 | ||
|  |       const encoder = new TextEncoder(); | ||
|  |       await conn.write(encoder.encode(emailContent)); | ||
|  |       await conn.write(encoder.encode('.\r\n')); | ||
|  | 
 | ||
|  |       const response = await readSmtpResponse(conn, '250'); | ||
|  |       assert(response.includes('250'), 'Should accept email with custom headers'); | ||
|  | 
 | ||
|  |       await sendSmtpCommand(conn, 'QUIT', '221'); | ||
|  |       console.log('✓ Successfully sent email with custom headers (X-Custom-Header, X-Priority, etc.)'); | ||
|  |     } finally { | ||
|  |       try { | ||
|  |         conn.close(); | ||
|  |       } catch { | ||
|  |         // Ignore
 | ||
|  |       } | ||
|  |     } | ||
|  |   }, | ||
|  |   sanitizeResources: false, | ||
|  |   sanitizeOps: false, | ||
|  | }); | ||
|  | 
 | ||
|  | Deno.test({ | ||
|  |   name: 'EP-01: Basic Email - send minimal email (no headers)', | ||
|  |   async fn() { | ||
|  |     const conn = await connectToSmtp('localhost', TEST_PORT); | ||
|  |     const fromAddress = 'sender@example.com'; | ||
|  |     const toAddress = 'recipient@example.com'; | ||
|  | 
 | ||
|  |     // Minimal email - just a body, no headers
 | ||
|  |     const emailContent = 'This is a minimal email with no headers.\r\n'; | ||
|  | 
 | ||
|  |     try { | ||
|  |       await waitForGreeting(conn); | ||
|  |       await sendSmtpCommand(conn, 'EHLO test.example.com', '250'); | ||
|  |       await sendSmtpCommand(conn, `MAIL FROM:<${fromAddress}>`, '250'); | ||
|  |       await sendSmtpCommand(conn, `RCPT TO:<${toAddress}>`, '250'); | ||
|  |       await sendSmtpCommand(conn, 'DATA', '354'); | ||
|  | 
 | ||
|  |       // Send minimal email
 | ||
|  |       const encoder = new TextEncoder(); | ||
|  |       await conn.write(encoder.encode(emailContent)); | ||
|  |       await conn.write(encoder.encode('.\r\n')); | ||
|  | 
 | ||
|  |       const response = await readSmtpResponse(conn, '250'); | ||
|  |       assert(response.includes('250'), 'Should accept minimal email'); | ||
|  | 
 | ||
|  |       await sendSmtpCommand(conn, 'QUIT', '221'); | ||
|  |       console.log('✓ Successfully sent minimal email (body only, no headers)'); | ||
|  |     } finally { | ||
|  |       try { | ||
|  |         conn.close(); | ||
|  |       } catch { | ||
|  |         // Ignore
 | ||
|  |       } | ||
|  |     } | ||
|  |   }, | ||
|  |   sanitizeResources: false, | ||
|  |   sanitizeOps: false, | ||
|  | }); | ||
|  | 
 | ||
|  | Deno.test({ | ||
|  |   name: 'EP-01: Cleanup - Stop SMTP server', | ||
|  |   async fn() { | ||
|  |     await stopTestServer(testServer); | ||
|  |   }, | ||
|  |   sanitizeResources: false, | ||
|  |   sanitizeOps: false, | ||
|  | }); |