/** * 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
This is the HTML version.
\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, });