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' ;
import { startTestServer , stopTestServer } from '../server.loader.js' ;
// Test configuration
const TEST_PORT = 2525 ;
const TEST_TIMEOUT = 60000 ; // Increased for large email handling
let testServer : any ;
// Setup
tap . test ( 'setup - start SMTP server' , async ( ) = > {
testServer = await startTestServer ( ) ;
await new Promise ( resolve = > setTimeout ( resolve , 1000 ) ) ;
} ) ;
// Test: Moderately large email (1MB)
tap . test ( 'Large Email - should handle 1MB email' , async ( tools ) = > {
const done = tools . defer ( ) ;
const socket = net . createConnection ( {
host : 'localhost' ,
port : TEST_PORT ,
timeout : TEST_TIMEOUT
} ) ;
let receivedData = '' ;
let currentStep = 'connecting' ;
let completed = false ;
// Generate 1MB of content
const largeBody = 'X' . repeat ( 1024 * 1024 ) ; // 1MB
const emailContent = ` Subject: 1MB Email Test \ r \ nFrom: sender@example.com \ r \ nTo: recipient@example.com \ r \ n \ r \ n ${ largeBody } \ 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:<sender@example.com>\r\n' ) ;
} else if ( currentStep === 'mail_from' && receivedData . includes ( '250' ) ) {
currentStep = 'rcpt_to' ;
socket . write ( 'RCPT TO:<recipient@example.com>\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 = 'sending_large_email' ;
// Send in chunks to avoid overwhelming
const chunkSize = 64 * 1024 ; // 64KB chunks
let sent = 0 ;
const sendChunk = ( ) = > {
if ( sent < emailContent . length ) {
const chunk = emailContent . slice ( sent , sent + chunkSize ) ;
socket . write ( chunk ) ;
sent += chunk . length ;
// Small delay between chunks
if ( sent < emailContent . length ) {
setTimeout ( sendChunk , 10 ) ;
} else {
// End of data
socket . write ( '.\r\n' ) ;
currentStep = 'sent' ;
}
}
} ;
sendChunk ( ) ;
} else if ( currentStep === 'sent' && ( receivedData . includes ( '250' ) || receivedData . includes ( '552' ) ) ) {
if ( ! completed ) {
completed = true ;
socket . write ( 'QUIT\r\n' ) ;
setTimeout ( ( ) = > {
socket . destroy ( ) ;
// Either accepted (250) or size exceeded (552)
expect ( receivedData ) . toMatch ( /250|552/ ) ;
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 email with MIME attachments
tap . test ( 'Large Email - should handle multi-part MIME message' , async ( tools ) = > {
const done = tools . defer ( ) ;
const socket = net . createConnection ( {
host : 'localhost' ,
port : TEST_PORT ,
timeout : TEST_TIMEOUT
} ) ;
let receivedData = '' ;
let currentStep = 'connecting' ;
let completed = false ;
const boundary = '----=_Part_0_123456789' ;
const attachment1 = 'A' . repeat ( 500 * 1024 ) ; // 500KB
const attachment2 = 'B' . repeat ( 300 * 1024 ) ; // 300KB
const emailContent = [
'Subject: Large MIME Email Test' ,
'From: sender@example.com' ,
'To: recipient@example.com' ,
'MIME-Version: 1.0' ,
` Content-Type: multipart/mixed; boundary=" ${ boundary } " ` ,
'' ,
'This is a multi-part message in MIME format.' ,
'' ,
` -- ${ boundary } ` ,
'Content-Type: text/plain; charset=utf-8' ,
'' ,
'This email contains large attachments.' ,
'' ,
` -- ${ boundary } ` ,
'Content-Type: text/plain; charset=utf-8' ,
'Content-Disposition: attachment; filename="file1.txt"' ,
'' ,
attachment1 ,
'' ,
` -- ${ boundary } ` ,
'Content-Type: application/octet-stream' ,
'Content-Disposition: attachment; filename="file2.bin"' ,
'Content-Transfer-Encoding: base64' ,
'' ,
Buffer . from ( attachment2 ) . toString ( 'base64' ) ,
'' ,
` -- ${ boundary } -- ` ,
''
] . join ( '\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:<sender@example.com>\r\n' ) ;
} else if ( currentStep === 'mail_from' && receivedData . includes ( '250' ) ) {
currentStep = 'rcpt_to' ;
socket . write ( 'RCPT TO:<recipient@example.com>\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 = 'sending_mime' ;
socket . write ( emailContent ) ;
socket . write ( '\r\n.\r\n' ) ;
currentStep = 'sent' ;
} else if ( currentStep === 'sent' && ( receivedData . includes ( '250' ) || receivedData . includes ( '552' ) ) ) {
if ( ! completed ) {
completed = true ;
socket . write ( 'QUIT\r\n' ) ;
setTimeout ( ( ) = > {
socket . destroy ( ) ;
expect ( receivedData ) . toMatch ( /250|552/ ) ;
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: Email size limits with SIZE extension
tap . test ( 'Large Email - should respect SIZE limits if advertised' , async ( tools ) = > {
const done = tools . defer ( ) ;
const socket = net . createConnection ( {
host : 'localhost' ,
port : TEST_PORT ,
timeout : TEST_TIMEOUT
} ) ;
let receivedData = '' ;
let currentStep = 'connecting' ;
let maxSize : number | null = null ;
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' ) ) {
// Check for SIZE extension
const sizeMatch = receivedData . match ( /SIZE\s+(\d+)/ ) ;
if ( sizeMatch ) {
maxSize = parseInt ( sizeMatch [ 1 ] ) ;
console . log ( ` Server advertises max size: ${ maxSize } bytes ` ) ;
}
currentStep = 'mail_from' ;
const emailSize = maxSize ? maxSize + 1000 : 5000000 ; // Over limit or 5MB
socket . write ( ` MAIL FROM:<sender@example.com> SIZE= ${ emailSize } \ r \ n ` ) ;
} else if ( currentStep === 'mail_from' ) {
if ( maxSize && receivedData . includes ( '552' ) ) {
// Size rejected - expected
socket . write ( 'QUIT\r\n' ) ;
setTimeout ( ( ) = > {
socket . destroy ( ) ;
expect ( receivedData ) . toInclude ( '552' ) ;
done . resolve ( ) ;
} , 100 ) ;
} else if ( receivedData . includes ( '250' ) ) {
// Size accepted or no limit
socket . write ( 'QUIT\r\n' ) ;
setTimeout ( ( ) = > {
socket . destroy ( ) ;
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: Very large email handling (5MB)
tap . test ( 'Large Email - should handle or reject very large emails gracefully' , async ( tools ) = > {
const done = tools . defer ( ) ;
const socket = net . createConnection ( {
host : 'localhost' ,
port : TEST_PORT ,
timeout : TEST_TIMEOUT
} ) ;
let receivedData = '' ;
let currentStep = 'connecting' ;
let completed = false ;
// Generate 5MB email
const largeContent = 'X' . repeat ( 5 * 1024 * 1024 ) ; // 5MB
const emailContent = ` Subject: 5MB Email Test \ r \ nFrom: sender@example.com \ r \ nTo: recipient@example.com \ r \ n \ r \ n ${ largeContent } \ 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:<sender@example.com>\r\n' ) ;
} else if ( currentStep === 'mail_from' && receivedData . includes ( '250' ) ) {
currentStep = 'rcpt_to' ;
socket . write ( 'RCPT TO:<recipient@example.com>\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 = 'sending_5mb' ;
console . log ( 'Sending 5MB email...' ) ;
// Send in larger chunks for efficiency
const chunkSize = 256 * 1024 ; // 256KB chunks
let sent = 0 ;
const sendChunk = ( ) = > {
if ( sent < emailContent . length ) {
const chunk = emailContent . slice ( sent , sent + chunkSize ) ;
socket . write ( chunk ) ;
sent += chunk . length ;
if ( sent < emailContent . length ) {
setImmediate ( sendChunk ) ; // Use setImmediate for better performance
} else {
socket . write ( '.\r\n' ) ;
currentStep = 'sent' ;
}
}
} ;
sendChunk ( ) ;
} else if ( currentStep === 'sent' ) {
const responseCode = receivedData . match ( /(\d{3})/ ) ? . [ 1 ] ;
if ( responseCode && ! completed ) {
completed = true ;
socket . write ( 'QUIT\r\n' ) ;
setTimeout ( ( ) = > {
socket . destroy ( ) ;
// Accept various responses: 250 (accepted), 552 (size exceeded), 554 (failed)
expect ( responseCode ) . toMatch ( /^(250|552|554|451|452)$/ ) ;
done . resolve ( ) ;
} , 100 ) ;
}
}
} ) ;
socket . on ( 'error' , ( error ) = > {
// Connection errors during large transfers are acceptable
if ( currentStep === 'sending_5mb' || currentStep === 'sent' ) {
done . resolve ( ) ;
} else {
done . reject ( error ) ;
}
} ) ;
socket . on ( 'timeout' , ( ) = > {
socket . destroy ( ) ;
done . reject ( new Error ( ` Connection timeout at step: ${ currentStep } ` ) ) ;
} ) ;
await done . promise ;
} ) ;
// Test: Chunked transfer handling
tap . test ( 'Large Email - should handle chunked transfers properly' , async ( tools ) = > {
const done = tools . defer ( ) ;
const socket = net . createConnection ( {
host : 'localhost' ,
port : TEST_PORT ,
timeout : TEST_TIMEOUT
} ) ;
let receivedData = '' ;
let currentStep = 'connecting' ;
let chunksSent = 0 ;
let completed = false ;
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:<recipient@example.com>\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 = 'chunked_sending' ;
// Send headers
socket . write ( 'Subject: Chunked Transfer Test\r\n' ) ;
socket . write ( 'From: sender@example.com\r\n' ) ;
socket . write ( 'To: recipient@example.com\r\n' ) ;
socket . write ( '\r\n' ) ;
// Send body in multiple chunks with delays
const chunks = [
'First chunk of data\r\n' ,
'Second chunk of data\r\n' ,
'Third chunk of data\r\n' ,
'Fourth chunk of data\r\n' ,
'Final chunk of data\r\n'
] ;
const sendNextChunk = ( ) = > {
if ( chunksSent < chunks . length ) {
socket . write ( chunks [ chunksSent ] ) ;
chunksSent ++ ;
setTimeout ( sendNextChunk , 100 ) ; // 100ms delay between chunks
} else {
socket . write ( '.\r\n' ) ;
}
} ;
sendNextChunk ( ) ;
} else if ( currentStep === 'chunked_sending' && receivedData . includes ( '250' ) ) {
if ( ! completed ) {
completed = true ;
socket . write ( 'QUIT\r\n' ) ;
setTimeout ( ( ) = > {
socket . destroy ( ) ;
expect ( chunksSent ) . toEqual ( 5 ) ;
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: Email with very long lines
tap . test ( 'Large Email - should handle emails with very long lines' , async ( tools ) = > {
const done = tools . defer ( ) ;
const socket = net . createConnection ( {
host : 'localhost' ,
port : TEST_PORT ,
timeout : TEST_TIMEOUT
} ) ;
let receivedData = '' ;
let currentStep = 'connecting' ;
let completed = false ;
// Create a very long line (10KB)
const veryLongLine = 'A' . repeat ( 10 * 1024 ) ;
const emailContent = ` Subject: Long Line Test \ r \ nFrom: sender@example.com \ r \ nTo: recipient@example.com \ r \ n \ r \ n ${ veryLongLine } \ r \ nNormal line after long line. \ 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:<sender@example.com>\r\n' ) ;
} else if ( currentStep === 'mail_from' && receivedData . includes ( '250' ) ) {
currentStep = 'rcpt_to' ;
socket . write ( 'RCPT TO:<recipient@example.com>\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 = 'long_line' ;
socket . write ( emailContent ) ;
socket . write ( '.\r\n' ) ;
currentStep = 'sent' ;
} else if ( currentStep === 'sent' ) {
const responseCode = receivedData . match ( /(\d{3})/ ) ? . [ 1 ] ;
if ( responseCode && ! completed ) {
completed = true ;
socket . write ( 'QUIT\r\n' ) ;
setTimeout ( ( ) = > {
socket . destroy ( ) ;
// May accept or reject based on line length limits
expect ( responseCode ) . toMatch ( /^(250|500|501|552)$/ ) ;
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 ) {
await stopTestServer ( ) ;
}
} ) ;
// Start the test
tap . start ( ) ;