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' ;
import * as path from 'path' ;
2025-05-24 00:23:35 +00:00
import { startTestServer , stopTestServer , type ITestServer } from '../../helpers/server.loader.js' ;
2025-05-23 19:03:44 +00:00
// Test configuration
2025-05-24 00:23:35 +00:00
const TEST_PORT = 30049 ;
2025-05-23 19:03:44 +00:00
const TEST_TIMEOUT = 15000 ;
2025-05-24 00:23:35 +00:00
let testServer : ITestServer ;
2025-05-23 19:03:44 +00:00
// Setup
tap . test ( 'setup - start SMTP server' , async ( ) = > {
2025-05-24 00:23:35 +00:00
testServer = await startTestServer ( { port : TEST_PORT , hostname : 'localhost' } ) ;
2025-05-23 19:03:44 +00:00
2025-05-24 00:23:35 +00:00
expect ( testServer ) . toBeDefined ( ) ;
2025-05-23 19:03:44 +00:00
expect ( testServer . port ) . toEqual ( TEST_PORT ) ;
} ) ;
// Test: Basic multiple recipients
tap . test ( 'Multiple Recipients - should accept multiple valid recipients' , async ( tools ) = > {
const done = tools . defer ( ) ;
const socket = net . createConnection ( {
host : 'localhost' ,
port : TEST_PORT ,
timeout : TEST_TIMEOUT
} ) ;
let receivedData = '' ;
let currentStep = 'connecting' ;
let recipientCount = 0 ;
const recipients = [
'recipient1@example.com' ,
'recipient2@example.com' ,
'recipient3@example.com'
] ;
let acceptedRecipients = 0 ;
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:<sender@example.com>\r\n' ) ;
} else if ( currentStep === 'mail_from' && receivedData . includes ( '250' ) ) {
currentStep = 'rcpt_to' ;
socket . write ( ` RCPT TO:< ${ recipients [ recipientCount ] } > \ r \ n ` ) ;
} else if ( currentStep === 'rcpt_to' ) {
2025-05-24 00:23:35 +00:00
if ( receivedData . includes ( '250' ) ) {
2025-05-23 19:03:44 +00:00
acceptedRecipients ++ ;
recipientCount ++ ;
if ( recipientCount < recipients . length ) {
receivedData = '' ; // Clear buffer for next response
socket . write ( ` RCPT TO:< ${ recipients [ recipientCount ] } > \ r \ n ` ) ;
} else {
currentStep = 'data' ;
socket . write ( 'DATA\r\n' ) ;
}
}
} else if ( currentStep === 'data' && receivedData . includes ( '354' ) ) {
currentStep = 'email_content' ;
const emailContent = ` Subject: Multiple Recipients Test \ r \ nFrom: sender@example.com \ r \ nTo: ${ recipients . join ( ', ' ) } \ r \ n \ r \ nThis email was sent to ${ acceptedRecipients } recipients. \ r \ n ` ;
socket . write ( emailContent ) ;
socket . write ( '\r\n.\r\n' ) ;
} else if ( currentStep === 'email_content' && receivedData . includes ( '250' ) ) {
socket . write ( 'QUIT\r\n' ) ;
setTimeout ( ( ) = > {
socket . destroy ( ) ;
expect ( acceptedRecipients ) . toEqual ( recipients . length ) ;
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: Mixed valid and invalid recipients
tap . test ( 'Multiple Recipients - should handle mix of valid and invalid recipients' , async ( tools ) = > {
const done = tools . defer ( ) ;
const socket = net . createConnection ( {
host : 'localhost' ,
port : TEST_PORT ,
timeout : TEST_TIMEOUT
} ) ;
let receivedData = '' ;
let currentStep = 'connecting' ;
let recipientIndex = 0 ;
const recipients = [
'valid@example.com' ,
'invalid-email' , // Invalid format
'another.valid@example.com' ,
'@example.com' , // Invalid format
'third.valid@example.com'
] ;
const recipientResults : Array < { email : string , accepted : boolean } > = [ ] ;
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:<sender@example.com>\r\n' ) ;
} else if ( currentStep === 'mail_from' && receivedData . includes ( '250' ) ) {
currentStep = 'rcpt_to' ;
socket . write ( ` RCPT TO:< ${ recipients [ recipientIndex ] } > \ r \ n ` ) ;
} else if ( currentStep === 'rcpt_to' ) {
const lines = receivedData . split ( '\r\n' ) ;
const lastLine = lines [ lines . length - 2 ] || lines [ lines . length - 1 ] ;
if ( lastLine . match ( /^\d{3}/ ) ) {
const accepted = lastLine . startsWith ( '250' ) ;
recipientResults . push ( {
email : recipients [ recipientIndex ] ,
accepted : accepted
} ) ;
recipientIndex ++ ;
if ( recipientIndex < recipients . length ) {
receivedData = '' ; // Clear buffer
socket . write ( ` RCPT TO:< ${ recipients [ recipientIndex ] } > \ r \ n ` ) ;
} else {
const acceptedCount = recipientResults . filter ( r = > r . accepted ) . length ;
if ( acceptedCount > 0 ) {
currentStep = 'data' ;
socket . write ( 'DATA\r\n' ) ;
} else {
socket . write ( 'QUIT\r\n' ) ;
setTimeout ( ( ) = > {
socket . destroy ( ) ;
expect ( acceptedCount ) . toEqual ( 0 ) ;
done . resolve ( ) ;
} , 100 ) ;
}
}
}
} else if ( currentStep === 'data' && receivedData . includes ( '354' ) ) {
currentStep = 'email_content' ;
const acceptedEmails = recipientResults . filter ( r = > r . accepted ) . map ( r = > r . email ) ;
const emailContent = ` Subject: Mixed Recipients Test \ r \ nFrom: sender@example.com \ r \ nTo: ${ acceptedEmails . join ( ', ' ) } \ r \ n \ r \ nDelivered to ${ acceptedEmails . length } valid recipients. \ r \ n ` ;
socket . write ( emailContent ) ;
socket . write ( '\r\n.\r\n' ) ;
} else if ( currentStep === 'email_content' && receivedData . includes ( '250' ) ) {
socket . write ( 'QUIT\r\n' ) ;
setTimeout ( ( ) = > {
socket . destroy ( ) ;
const acceptedCount = recipientResults . filter ( r = > r . accepted ) . length ;
const rejectedCount = recipientResults . filter ( r = > ! r . accepted ) . length ;
expect ( acceptedCount ) . toEqual ( 3 ) ; // 3 valid recipients
expect ( rejectedCount ) . toEqual ( 2 ) ; // 2 invalid recipients
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: Large number of recipients
tap . test ( 'Multiple Recipients - should handle many recipients' , async ( tools ) = > {
const done = tools . defer ( ) ;
const socket = net . createConnection ( {
host : 'localhost' ,
port : TEST_PORT ,
timeout : TEST_TIMEOUT
} ) ;
let receivedData = '' ;
let currentStep = 'connecting' ;
let recipientCount = 0 ;
const totalRecipients = 10 ;
const recipients : string [ ] = [ ] ;
for ( let i = 1 ; i <= totalRecipients ; i ++ ) {
recipients . push ( ` recipient ${ i } @example.com ` ) ;
}
let acceptedCount = 0 ;
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:<sender@example.com>\r\n' ) ;
} else if ( currentStep === 'mail_from' && receivedData . includes ( '250' ) ) {
currentStep = 'rcpt_to' ;
socket . write ( ` RCPT TO:< ${ recipients [ recipientCount ] } > \ r \ n ` ) ;
} else if ( currentStep === 'rcpt_to' ) {
if ( receivedData . includes ( '250' ) ) {
acceptedCount ++ ;
}
recipientCount ++ ;
if ( recipientCount < recipients . length ) {
receivedData = '' ; // Clear buffer
socket . write ( ` RCPT TO:< ${ recipients [ recipientCount ] } > \ r \ n ` ) ;
} else {
currentStep = 'data' ;
socket . write ( 'DATA\r\n' ) ;
}
} else if ( currentStep === 'data' && receivedData . includes ( '354' ) ) {
currentStep = 'email_content' ;
const emailContent = ` Subject: Large Recipients Test \ r \ nFrom: sender@example.com \ r \ n \ r \ nSent to ${ acceptedCount } recipients. \ r \ n ` ;
socket . write ( emailContent ) ;
socket . write ( '\r\n.\r\n' ) ;
} else if ( currentStep === 'email_content' && receivedData . includes ( '250' ) ) {
socket . write ( 'QUIT\r\n' ) ;
setTimeout ( ( ) = > {
socket . destroy ( ) ;
expect ( acceptedCount ) . toBeGreaterThan ( 0 ) ;
expect ( acceptedCount ) . toBeLessThan ( totalRecipients + 1 ) ;
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: Duplicate recipients
tap . test ( 'Multiple Recipients - should handle duplicate recipients' , async ( tools ) = > {
const done = tools . defer ( ) ;
const socket = net . createConnection ( {
host : 'localhost' ,
port : TEST_PORT ,
timeout : TEST_TIMEOUT
} ) ;
let receivedData = '' ;
let currentStep = 'connecting' ;
let recipientCount = 0 ;
const recipients = [
'duplicate@example.com' ,
'unique@example.com' ,
'duplicate@example.com' , // Duplicate
'another@example.com' ,
'duplicate@example.com' // Another duplicate
] ;
const results : boolean [ ] = [ ] ;
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:<sender@example.com>\r\n' ) ;
} else if ( currentStep === 'mail_from' && receivedData . includes ( '250' ) ) {
currentStep = 'rcpt_to' ;
socket . write ( ` RCPT TO:< ${ recipients [ recipientCount ] } > \ r \ n ` ) ;
} else if ( currentStep === 'rcpt_to' ) {
if ( receivedData . match ( /[245]\d{2}/ ) ) {
results . push ( receivedData . includes ( '250' ) ) ;
recipientCount ++ ;
if ( recipientCount < recipients . length ) {
receivedData = '' ; // Clear buffer
socket . write ( ` RCPT TO:< ${ recipients [ recipientCount ] } > \ r \ n ` ) ;
} else {
currentStep = 'data' ;
socket . write ( 'DATA\r\n' ) ;
}
}
} else if ( currentStep === 'data' && receivedData . includes ( '354' ) ) {
currentStep = 'email_content' ;
const emailContent = ` Subject: Duplicate Recipients Test \ r \ nFrom: sender@example.com \ r \ n \ r \ nTesting duplicate recipient handling. \ r \ n ` ;
socket . write ( emailContent ) ;
socket . write ( '\r\n.\r\n' ) ;
} else if ( currentStep === 'email_content' && receivedData . includes ( '250' ) ) {
socket . write ( 'QUIT\r\n' ) ;
setTimeout ( ( ) = > {
socket . destroy ( ) ;
expect ( results . length ) . toEqual ( recipients . length ) ;
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: No recipients (should fail DATA)
tap . test ( 'Multiple Recipients - DATA should fail with no recipients' , async ( tools ) = > {
const done = tools . defer ( ) ;
const socket = net . createConnection ( {
host : 'localhost' ,
port : TEST_PORT ,
timeout : TEST_TIMEOUT
} ) ;
let receivedData = '' ;
let currentStep = 'connecting' ;
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:<sender@example.com>\r\n' ) ;
} else if ( currentStep === 'mail_from' && receivedData . includes ( '250' ) ) {
// Skip RCPT TO, go directly to DATA
currentStep = 'data_no_recipients' ;
socket . write ( 'DATA\r\n' ) ;
} else if ( currentStep === 'data_no_recipients' && receivedData . includes ( '503' ) ) {
socket . write ( 'QUIT\r\n' ) ;
setTimeout ( ( ) = > {
socket . destroy ( ) ;
expect ( receivedData ) . toInclude ( '503' ) ; // Bad sequence
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: Recipients with different domains
tap . test ( 'Multiple Recipients - should handle recipients from different domains' , async ( tools ) = > {
const done = tools . defer ( ) ;
const socket = net . createConnection ( {
host : 'localhost' ,
port : TEST_PORT ,
timeout : TEST_TIMEOUT
} ) ;
let receivedData = '' ;
let currentStep = 'connecting' ;
let recipientCount = 0 ;
const recipients = [
'user1@example.com' ,
'user2@test.com' ,
'user3@localhost' ,
'user4@example.org' ,
'user5@subdomain.example.com'
] ;
let acceptedCount = 0 ;
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:<sender@example.com>\r\n' ) ;
} else if ( currentStep === 'mail_from' && receivedData . includes ( '250' ) ) {
currentStep = 'rcpt_to' ;
socket . write ( ` RCPT TO:< ${ recipients [ recipientCount ] } > \ r \ n ` ) ;
} else if ( currentStep === 'rcpt_to' ) {
if ( receivedData . includes ( '250' ) ) {
acceptedCount ++ ;
}
recipientCount ++ ;
if ( recipientCount < recipients . length ) {
receivedData = '' ; // Clear buffer
socket . write ( ` RCPT TO:< ${ recipients [ recipientCount ] } > \ r \ n ` ) ;
} else {
if ( acceptedCount > 0 ) {
currentStep = 'data' ;
socket . write ( 'DATA\r\n' ) ;
} else {
socket . write ( 'QUIT\r\n' ) ;
setTimeout ( ( ) = > {
socket . destroy ( ) ;
done . resolve ( ) ;
} , 100 ) ;
}
}
} else if ( currentStep === 'data' && receivedData . includes ( '354' ) ) {
currentStep = 'email_content' ;
const emailContent = ` Subject: Multi-domain Test \ r \ nFrom: sender@example.com \ r \ n \ r \ nDelivered to ${ acceptedCount } recipients across different domains. \ r \ n ` ;
socket . write ( emailContent ) ;
socket . write ( '\r\n.\r\n' ) ;
} else if ( currentStep === 'email_content' && receivedData . includes ( '250' ) ) {
socket . write ( 'QUIT\r\n' ) ;
setTimeout ( ( ) = > {
socket . destroy ( ) ;
expect ( acceptedCount ) . toBeGreaterThan ( 0 ) ;
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 ( ) = > {
if ( testServer ) {
2025-05-24 00:23:35 +00:00
await stopTestServer ( testServer ) ;
2025-05-23 19:03:44 +00:00
}
2025-05-24 00:23:35 +00:00
expect ( true ) . toEqual ( true ) ;
2025-05-23 19:03:44 +00:00
} ) ;
// Start the test
tap . start ( ) ;