2025-05-23 19:09:30 +00:00
import { tap , expect } from '@git.zone/tstest/tapbundle' ;
2025-05-23 19:03:44 +00:00
import * as net from 'net' ;
2025-05-23 21:20:39 +00:00
import { startTestServer , stopTestServer } from '../../helpers/server.loader.js' ;
2025-05-23 19:49:25 +00:00
import type { SmtpServer } from '../../../ts/mail/delivery/smtpserver/index.js' ;
2025-05-23 19:03:44 +00:00
// Test configuration
const TEST_PORT = 2525 ;
const TEST_TIMEOUT = 15000 ;
2025-05-23 19:49:25 +00:00
let testServer : SmtpServer ;
2025-05-23 19:03:44 +00:00
// Setup
tap . test ( 'setup - start SMTP server' , async ( ) = > {
testServer = await startTestServer ( ) ;
await new Promise ( resolve = > setTimeout ( resolve , 1000 ) ) ;
} ) ;
// Test: Complete email sending flow
tap . test ( 'Basic Email Sending - should send email through complete SMTP flow' , async ( tools ) = > {
const done = tools . defer ( ) ;
const socket = net . createConnection ( {
host : 'localhost' ,
port : TEST_PORT ,
timeout : TEST_TIMEOUT
} ) ;
let receivedData = '' ;
let currentStep = 'connecting' ;
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 ` ;
const steps : string [ ] = [ ] ;
socket . on ( 'data' , ( data ) = > {
receivedData += data . toString ( ) ;
if ( currentStep === 'connecting' && receivedData . includes ( '220' ) ) {
steps . push ( 'CONNECT' ) ;
currentStep = 'ehlo' ;
socket . write ( 'EHLO test.example.com\r\n' ) ;
} else if ( currentStep === 'ehlo' && receivedData . includes ( '250' ) ) {
steps . push ( 'EHLO' ) ;
currentStep = 'mail_from' ;
socket . write ( ` MAIL FROM:< ${ fromAddress } > \ r \ n ` ) ;
} else if ( currentStep === 'mail_from' && receivedData . includes ( '250' ) ) {
steps . push ( 'MAIL FROM' ) ;
currentStep = 'rcpt_to' ;
socket . write ( ` RCPT TO:< ${ toAddress } > \ r \ n ` ) ;
} else if ( currentStep === 'rcpt_to' && receivedData . includes ( '250' ) ) {
steps . push ( 'RCPT TO' ) ;
currentStep = 'data' ;
socket . write ( 'DATA\r\n' ) ;
} else if ( currentStep === 'data' && receivedData . includes ( '354' ) ) {
steps . push ( 'DATA' ) ;
currentStep = 'email_content' ;
socket . write ( emailContent ) ;
socket . write ( '\r\n.\r\n' ) ; // End of data marker
} else if ( currentStep === 'email_content' && receivedData . includes ( '250' ) ) {
steps . push ( 'CONTENT' ) ;
currentStep = 'quit' ;
socket . write ( 'QUIT\r\n' ) ;
} else if ( currentStep === 'quit' && receivedData . includes ( '221' ) ) {
steps . push ( 'QUIT' ) ;
socket . destroy ( ) ;
// Verify all steps completed
expect ( steps ) . toInclude ( 'CONNECT' ) ;
expect ( steps ) . toInclude ( 'EHLO' ) ;
expect ( steps ) . toInclude ( 'MAIL FROM' ) ;
expect ( steps ) . toInclude ( 'RCPT TO' ) ;
expect ( steps ) . toInclude ( 'DATA' ) ;
expect ( steps ) . toInclude ( 'CONTENT' ) ;
expect ( steps ) . toInclude ( 'QUIT' ) ;
expect ( steps . length ) . toEqual ( 7 ) ;
done . resolve ( ) ;
} else if ( receivedData . match ( /\r\n5\d{2}\s/ ) ) {
// Server error (5xx response codes)
socket . destroy ( ) ;
done . reject ( new Error ( ` Email sending failed at step ${ currentStep } : ${ receivedData } ` ) ) ;
}
} ) ;
socket . on ( 'error' , ( error ) = > {
done . reject ( error ) ;
} ) ;
socket . on ( 'timeout' , ( ) = > {
socket . destroy ( ) ;
done . reject ( new Error ( ` Connection timeout at step: ${ currentStep } ` ) ) ;
} ) ;
await done . promise ;
} ) ;
// Test: Send email with attachments (MIME)
tap . test ( 'Basic Email Sending - should send email with MIME attachment' , async ( tools ) = > {
const done = tools . defer ( ) ;
const socket = net . createConnection ( {
host : 'localhost' ,
port : TEST_PORT ,
timeout : TEST_TIMEOUT
} ) ;
let receivedData = '' ;
let currentStep = 'connecting' ;
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 ` ;
socket . on ( 'data' , ( data ) = > {
receivedData += data . toString ( ) ;
if ( currentStep === 'connecting' && receivedData . includes ( '220' ) ) {
currentStep = 'ehlo' ;
socket . write ( 'EHLO test.example.com\r\n' ) ;
} else if ( currentStep === 'ehlo' && receivedData . includes ( '250' ) ) {
currentStep = 'mail_from' ;
socket . write ( ` MAIL FROM:< ${ fromAddress } > \ r \ n ` ) ;
} else if ( currentStep === 'mail_from' && receivedData . includes ( '250' ) ) {
currentStep = 'rcpt_to' ;
socket . write ( ` RCPT TO:< ${ toAddress } > \ r \ n ` ) ;
} else if ( currentStep === 'rcpt_to' && receivedData . includes ( '250' ) ) {
currentStep = 'data' ;
socket . write ( 'DATA\r\n' ) ;
} else if ( currentStep === 'data' && receivedData . includes ( '354' ) ) {
currentStep = 'email_content' ;
socket . write ( emailContent ) ;
socket . write ( '\r\n.\r\n' ) ; // End of data marker
} else if ( currentStep === 'email_content' && receivedData . includes ( '250' ) ) {
socket . write ( 'QUIT\r\n' ) ;
setTimeout ( ( ) = > {
socket . destroy ( ) ;
expect ( receivedData ) . toInclude ( '250' ) ;
done . resolve ( ) ;
} , 100 ) ;
}
} ) ;
socket . on ( 'error' , ( error ) = > {
done . reject ( error ) ;
} ) ;
socket . on ( 'timeout' , ( ) = > {
socket . destroy ( ) ;
done . reject ( new Error ( ` Connection timeout at step: ${ currentStep } ` ) ) ;
} ) ;
await done . promise ;
} ) ;
// Test: Send HTML email
tap . test ( 'Basic Email Sending - should send HTML email' , async ( tools ) = > {
const done = tools . defer ( ) ;
const socket = net . createConnection ( {
host : 'localhost' ,
port : TEST_PORT ,
timeout : TEST_TIMEOUT
} ) ;
let receivedData = '' ;
let currentStep = 'connecting' ;
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 ` ;
socket . on ( 'data' , ( data ) = > {
receivedData += data . toString ( ) ;
if ( currentStep === 'connecting' && receivedData . includes ( '220' ) ) {
currentStep = 'ehlo' ;
socket . write ( 'EHLO test.example.com\r\n' ) ;
} else if ( currentStep === 'ehlo' && receivedData . includes ( '250' ) ) {
currentStep = 'mail_from' ;
socket . write ( ` MAIL FROM:< ${ fromAddress } > \ r \ n ` ) ;
} else if ( currentStep === 'mail_from' && receivedData . includes ( '250' ) ) {
currentStep = 'rcpt_to' ;
socket . write ( ` RCPT TO:< ${ toAddress } > \ r \ n ` ) ;
} else if ( currentStep === 'rcpt_to' && receivedData . includes ( '250' ) ) {
currentStep = 'data' ;
socket . write ( 'DATA\r\n' ) ;
} else if ( currentStep === 'data' && receivedData . includes ( '354' ) ) {
currentStep = 'email_content' ;
socket . write ( emailContent ) ;
socket . write ( '\r\n.\r\n' ) ; // End of data marker
} else if ( currentStep === 'email_content' && receivedData . includes ( '250' ) ) {
socket . write ( 'QUIT\r\n' ) ;
setTimeout ( ( ) = > {
socket . destroy ( ) ;
expect ( receivedData ) . toInclude ( '250' ) ;
done . resolve ( ) ;
} , 100 ) ;
}
} ) ;
socket . on ( 'error' , ( error ) = > {
done . reject ( error ) ;
} ) ;
socket . on ( 'timeout' , ( ) = > {
socket . destroy ( ) ;
done . reject ( new Error ( ` Connection timeout at step: ${ currentStep } ` ) ) ;
} ) ;
await done . promise ;
} ) ;
// Test: Send email with custom headers
tap . test ( 'Basic Email Sending - should send email with custom headers' , async ( tools ) = > {
const done = tools . defer ( ) ;
const socket = net . createConnection ( {
host : 'localhost' ,
port : TEST_PORT ,
timeout : TEST_TIMEOUT
} ) ;
let receivedData = '' ;
let currentStep = 'connecting' ;
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 ` ;
socket . on ( 'data' , ( data ) = > {
receivedData += data . toString ( ) ;
if ( currentStep === 'connecting' && receivedData . includes ( '220' ) ) {
currentStep = 'ehlo' ;
socket . write ( 'EHLO test.example.com\r\n' ) ;
} else if ( currentStep === 'ehlo' && receivedData . includes ( '250' ) ) {
currentStep = 'mail_from' ;
socket . write ( ` MAIL FROM:< ${ fromAddress } > \ r \ n ` ) ;
} else if ( currentStep === 'mail_from' && receivedData . includes ( '250' ) ) {
currentStep = 'rcpt_to' ;
socket . write ( ` RCPT TO:< ${ toAddress } > \ r \ n ` ) ;
} else if ( currentStep === 'rcpt_to' && receivedData . includes ( '250' ) ) {
currentStep = 'data' ;
socket . write ( 'DATA\r\n' ) ;
} else if ( currentStep === 'data' && receivedData . includes ( '354' ) ) {
currentStep = 'email_content' ;
socket . write ( emailContent ) ;
socket . write ( '\r\n.\r\n' ) ; // End of data marker
} else if ( currentStep === 'email_content' && receivedData . includes ( '250' ) ) {
socket . write ( 'QUIT\r\n' ) ;
setTimeout ( ( ) = > {
socket . destroy ( ) ;
expect ( receivedData ) . toInclude ( '250' ) ;
done . resolve ( ) ;
} , 100 ) ;
}
} ) ;
socket . on ( 'error' , ( error ) = > {
done . reject ( error ) ;
} ) ;
socket . on ( 'timeout' , ( ) = > {
socket . destroy ( ) ;
done . reject ( new Error ( ` Connection timeout at step: ${ currentStep } ` ) ) ;
} ) ;
await done . promise ;
} ) ;
// Test: Minimal email (only required headers)
tap . test ( 'Basic Email Sending - should send minimal email' , async ( tools ) = > {
const done = tools . defer ( ) ;
const socket = net . createConnection ( {
host : 'localhost' ,
port : TEST_PORT ,
timeout : TEST_TIMEOUT
} ) ;
let receivedData = '' ;
let currentStep = 'connecting' ;
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' ;
socket . on ( 'data' , ( data ) = > {
receivedData += data . toString ( ) ;
if ( currentStep === 'connecting' && receivedData . includes ( '220' ) ) {
currentStep = 'ehlo' ;
socket . write ( 'EHLO test.example.com\r\n' ) ;
} else if ( currentStep === 'ehlo' && receivedData . includes ( '250' ) ) {
currentStep = 'mail_from' ;
socket . write ( ` MAIL FROM:< ${ fromAddress } > \ r \ n ` ) ;
} else if ( currentStep === 'mail_from' && receivedData . includes ( '250' ) ) {
currentStep = 'rcpt_to' ;
socket . write ( ` RCPT TO:< ${ toAddress } > \ r \ n ` ) ;
} else if ( currentStep === 'rcpt_to' && receivedData . includes ( '250' ) ) {
currentStep = 'data' ;
socket . write ( 'DATA\r\n' ) ;
} else if ( currentStep === 'data' && receivedData . includes ( '354' ) ) {
currentStep = 'email_content' ;
socket . write ( emailContent ) ;
socket . write ( '\r\n.\r\n' ) ; // End of data marker
} else if ( currentStep === 'email_content' && receivedData . includes ( '250' ) ) {
socket . write ( 'QUIT\r\n' ) ;
setTimeout ( ( ) = > {
socket . destroy ( ) ;
expect ( receivedData ) . toInclude ( '250' ) ;
done . resolve ( ) ;
} , 100 ) ;
}
} ) ;
socket . on ( 'error' , ( error ) = > {
done . reject ( error ) ;
} ) ;
socket . on ( 'timeout' , ( ) = > {
socket . destroy ( ) ;
done . reject ( new Error ( ` Connection timeout at step: ${ currentStep } ` ) ) ;
} ) ;
await done . promise ;
} ) ;
// Teardown
tap . test ( 'teardown - stop SMTP server' , async ( ) = > {
await stopTestServer ( ) ;
} ) ;
// Start the test
tap . start ( ) ;