This commit is contained in:
2025-05-24 14:39:48 +00:00
parent dc5c0b2584
commit 6e19e30f87
80 changed files with 0 additions and 0 deletions

View File

@ -0,0 +1,338 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as net from 'net';
import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'
import type { ITestServer } from '../../helpers/server.loader.js';
// Test configuration
const TEST_PORT = 2525;
const TEST_TIMEOUT = 15000;
let testServer: ITestServer;
// Setup
tap.test('setup - start SMTP server', async () => {
testServer = await startTestServer({ port: TEST_PORT });
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(testServer);
});
// Start the test
tap.start();

View File

@ -0,0 +1,315 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as net from 'net';
import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'
import type { ITestServer } from '../../helpers/server.loader.js';
// Test configuration
const TEST_PORT = 2525;
const TEST_TIMEOUT = 20000;
let testServer: ITestServer;
// Setup
tap.test('setup - start SMTP server', async () => {
testServer = await startTestServer({ port: TEST_PORT });
await new Promise(resolve => setTimeout(resolve, 1000));
});
// Test: Invalid email address validation
tap.test('Invalid Email Addresses - should reject various invalid email formats', async (tools) => {
const done = tools.defer();
const invalidAddresses = [
'invalid-email',
'@example.com',
'user@',
'user..name@example.com',
'user@.example.com',
'user@example..com',
'user@example.',
'user name@example.com',
'user@exam ple.com',
'user@[invalid]',
'a'.repeat(65) + '@example.com', // Local part too long
'user@' + 'a'.repeat(250) + '.com' // Domain too long
];
const results: Array<{
address: string;
response: string;
responseCode: string;
properlyRejected: boolean;
accepted: boolean;
}> = [];
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: TEST_TIMEOUT
});
let currentIndex = 0;
let state = 'connecting';
let buffer = '';
let lastResponseCode = '';
const fromAddress = 'test@example.com';
const processNextAddress = () => {
if (currentIndex < invalidAddresses.length) {
socket.write(`RCPT TO:<${invalidAddresses[currentIndex]}>\r\n`);
state = 'rcpt';
} else {
socket.write('QUIT\r\n');
state = 'quit';
}
};
socket.on('data', (data) => {
buffer += data.toString();
const lines = buffer.split('\r\n');
// Process complete lines
for (let i = 0; i < lines.length - 1; i++) {
const line = lines[i];
if (line.match(/^\d{3}/)) {
lastResponseCode = line.substring(0, 3);
if (state === 'connecting' && line.startsWith('220')) {
socket.write('EHLO test.example.com\r\n');
state = 'ehlo';
} else if (state === 'ehlo' && line.startsWith('250') && !line.includes('250-')) {
socket.write(`MAIL FROM:<${fromAddress}>\r\n`);
state = 'mail';
} else if (state === 'mail' && line.startsWith('250')) {
processNextAddress();
} else if (state === 'rcpt') {
// Record result
const rejected = lastResponseCode.startsWith('5') || lastResponseCode.startsWith('4');
results.push({
address: invalidAddresses[currentIndex],
response: line,
responseCode: lastResponseCode,
properlyRejected: rejected,
accepted: lastResponseCode.startsWith('2')
});
currentIndex++;
if (currentIndex < invalidAddresses.length) {
// Reset and test next
socket.write('RSET\r\n');
state = 'rset';
} else {
socket.write('QUIT\r\n');
state = 'quit';
}
} else if (state === 'rset' && line.startsWith('250')) {
socket.write(`MAIL FROM:<${fromAddress}>\r\n`);
state = 'mail';
} else if (state === 'quit' && line.startsWith('221')) {
socket.destroy();
// Analyze results
const rejected = results.filter(r => r.properlyRejected).length;
const rate = results.length > 0 ? rejected / results.length : 0;
// Log results for debugging
results.forEach(r => {
if (!r.properlyRejected) {
console.log(`WARNING: Invalid address accepted: ${r.address}`);
}
});
// We expect at least 70% rejection rate for invalid addresses
expect(rate).toBeGreaterThan(0.7);
expect(results.length).toEqual(invalidAddresses.length);
done.resolve();
}
}
}
// Keep incomplete line in buffer
buffer = lines[lines.length - 1];
});
socket.on('timeout', () => {
socket.destroy();
done.reject(new Error('Test timeout'));
});
socket.on('error', (err) => {
done.reject(err);
});
await done.promise;
});
// Test: Edge case email addresses that might be valid
tap.test('Invalid Email Addresses - should handle edge case addresses', async (tools) => {
const done = tools.defer();
const edgeCaseAddresses = [
'user+tag@example.com', // Valid - with plus addressing
'user.name@example.com', // Valid - with dot
'user@sub.example.com', // Valid - subdomain
'user@192.168.1.1', // Valid - IP address
'user@[192.168.1.1]', // Valid - IP in brackets
'"user name"@example.com', // Valid - quoted local part
'user\\@name@example.com', // Valid - escaped character
'user@localhost', // Might be valid depending on server config
];
const results: Array<{
address: string;
accepted: boolean;
}> = [];
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: TEST_TIMEOUT
});
let currentIndex = 0;
let state = 'connecting';
let buffer = '';
const fromAddress = 'test@example.com';
socket.on('data', (data) => {
buffer += data.toString();
const lines = buffer.split('\r\n');
for (let i = 0; i < lines.length - 1; i++) {
const line = lines[i];
if (line.match(/^\d{3}/)) {
const responseCode = line.substring(0, 3);
if (state === 'connecting' && line.startsWith('220')) {
socket.write('EHLO test.example.com\r\n');
state = 'ehlo';
} else if (state === 'ehlo' && line.startsWith('250') && !line.includes('250-')) {
socket.write(`MAIL FROM:<${fromAddress}>\r\n`);
state = 'mail';
} else if (state === 'mail' && line.startsWith('250')) {
if (currentIndex < edgeCaseAddresses.length) {
socket.write(`RCPT TO:<${edgeCaseAddresses[currentIndex]}>\r\n`);
state = 'rcpt';
} else {
socket.write('QUIT\r\n');
state = 'quit';
}
} else if (state === 'rcpt') {
results.push({
address: edgeCaseAddresses[currentIndex],
accepted: responseCode.startsWith('2')
});
currentIndex++;
if (currentIndex < edgeCaseAddresses.length) {
socket.write('RSET\r\n');
state = 'rset';
} else {
socket.write('QUIT\r\n');
state = 'quit';
}
} else if (state === 'rset' && line.startsWith('250')) {
socket.write(`MAIL FROM:<${fromAddress}>\r\n`);
state = 'mail';
} else if (state === 'quit' && line.startsWith('221')) {
socket.destroy();
// Just verify we tested all addresses
expect(results.length).toEqual(edgeCaseAddresses.length);
done.resolve();
}
}
}
buffer = lines[lines.length - 1];
});
socket.on('timeout', () => {
socket.destroy();
done.reject(new Error('Test timeout'));
});
socket.on('error', (err) => {
done.reject(err);
});
await done.promise;
});
// Test: Empty and null addresses
tap.test('Invalid Email Addresses - should handle empty addresses', 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:<test@example.com>\r\n');
} else if (currentStep === 'mail_from' && receivedData.includes('250')) {
currentStep = 'rcpt_empty';
socket.write('RCPT TO:<>\r\n'); // Empty address
} else if (currentStep === 'rcpt_empty') {
if (receivedData.includes('250')) {
// Empty recipient allowed (for bounces)
currentStep = 'rset';
socket.write('RSET\r\n');
} else if (receivedData.match(/[45]\d{2}/)) {
// Empty recipient rejected
currentStep = 'rset';
socket.write('RSET\r\n');
}
} else if (currentStep === 'rset' && receivedData.includes('250')) {
currentStep = 'mail_empty';
socket.write('MAIL FROM:<>\r\n'); // Empty sender (bounce)
} else if (currentStep === 'mail_empty' && receivedData.includes('250')) {
currentStep = 'rcpt_after_empty';
socket.write('RCPT TO:<recipient@example.com>\r\n');
} else if (currentStep === 'rcpt_after_empty' && receivedData.includes('250')) {
socket.write('QUIT\r\n');
setTimeout(() => {
socket.destroy();
// Empty MAIL FROM should be accepted for bounces
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(testServer);
});
// Start the test
tap.start();

View File

@ -0,0 +1,493 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as net from 'net';
import * as path from 'path';
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
// Test configuration
const TEST_PORT = 30049;
const TEST_TIMEOUT = 15000;
let testServer: ITestServer;
// Setup
tap.test('setup - start SMTP server', async () => {
testServer = await startTestServer({ port: TEST_PORT, hostname: 'localhost' });
expect(testServer).toBeDefined();
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') {
if (receivedData.includes('250')) {
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') {
if (receivedData.includes('503')) {
// Expected: bad sequence error
socket.write('QUIT\r\n');
setTimeout(() => {
socket.destroy();
expect(receivedData).toInclude('503'); // Bad sequence
done.resolve();
}, 100);
} else if (receivedData.includes('354')) {
// Some servers accept DATA without recipients and fail later
// Send empty data to trigger the error
socket.write('.\r\n');
currentStep = 'data_sent';
}
} else if (currentStep === 'data_sent' && receivedData.match(/[45]\d{2}/)) {
socket.write('QUIT\r\n');
setTimeout(() => {
socket.destroy();
// Should get an error when trying to send without recipients
expect(receivedData).toMatch(/[45]\d{2}/);
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) {
await stopTestServer(testServer);
}
expect(true).toEqual(true);
});
// Start the test
tap.start();

View File

@ -0,0 +1,528 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as net from 'net';
import * as path from 'path';
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
// Test configuration
const TEST_PORT = 30048;
const TEST_TIMEOUT = 60000; // Increased for large email handling
let testServer: ITestServer;
// Setup
tap.test('setup - start SMTP server', async () => {
testServer = await startTestServer({ port: TEST_PORT, hostname: 'localhost' });
expect(testServer).toBeDefined();
});
// 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' && receivedData.match(/[245]\d{2}/)) {
if (!completed) {
completed = true;
// Extract the last response code
const lines = receivedData.split('\r\n');
let responseCode = '';
// Look for the most recent response code
for (let i = lines.length - 1; i >= 0; i--) {
const match = lines[i].match(/^([245]\d{2})[\s-]/);
if (match) {
responseCode = match[1];
break;
}
}
// If we couldn't extract, but we know there's a response, default to 250
if (!responseCode && receivedData.includes('250 OK message queued')) {
responseCode = '250';
}
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') {
// Extract the last response code from the received data
// Look for response codes that are at the beginning of a line
const responseMatches = receivedData.split('\r\n').filter(line => /^\d{3}\s/.test(line));
const lastResponseLine = responseMatches[responseMatches.length - 1];
const responseCode = lastResponseLine?.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(testServer);
}
expect(true).toEqual(true);
});
// Start the test
tap.start();

View File

@ -0,0 +1,515 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as net from 'net';
import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'
import type { ITestServer } from '../../helpers/server.loader.js';
const TEST_PORT = 2525;
let testServer: ITestServer;
tap.test('setup - start test server', async () => {
testServer = await startTestServer({ port: TEST_PORT });
await new Promise(resolve => setTimeout(resolve, 1000));
});
tap.test('MIME Handling - Comprehensive multipart message', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
let dataBuffer = '';
let step = 'greeting';
let completed = false;
socket.on('data', (data) => {
dataBuffer += data.toString();
console.log('Server response:', data.toString());
if (step === 'greeting' && dataBuffer.includes('220 ')) {
step = 'ehlo';
socket.write('EHLO testclient\r\n');
dataBuffer = '';
} else if (step === 'ehlo' && dataBuffer.includes('250')) {
step = 'mail';
socket.write('MAIL FROM:<sender@example.com>\r\n');
dataBuffer = '';
} else if (step === 'mail' && dataBuffer.includes('250')) {
step = 'rcpt';
socket.write('RCPT TO:<recipient@example.com>\r\n');
dataBuffer = '';
} else if (step === 'rcpt' && dataBuffer.includes('250')) {
step = 'data';
socket.write('DATA\r\n');
dataBuffer = '';
} else if (step === 'data' && dataBuffer.includes('354')) {
// Create comprehensive MIME test email
const boundary = 'mime-test-boundary-12345';
const innerBoundary = 'inner-mime-boundary-67890';
const email = [
`From: sender@example.com`,
`To: recipient@example.com`,
`Subject: MIME Handling Test - Comprehensive`,
`Date: ${new Date().toUTCString()}`,
`Message-ID: <mime-test-${Date.now()}@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`,
`Content-Transfer-Encoding: 7bit`,
'',
'This is the plain text part of the email.',
'It tests basic MIME text handling.',
'',
`--${boundary}`,
`Content-Type: text/html; charset=utf-8`,
`Content-Transfer-Encoding: quoted-printable`,
'',
'<html>',
'<head><title>MIME Test</title></head>',
'<body>',
'<h1>HTML MIME Content</h1>',
'<p>This tests HTML MIME content handling.</p>',
'<p>Special chars: =E2=98=85 =E2=9C=93 =E2=9D=A4</p>',
'</body>',
'</html>',
'',
`--${boundary}`,
`Content-Type: multipart/alternative; boundary="${innerBoundary}"`,
'',
`--${innerBoundary}`,
`Content-Type: text/plain; charset=iso-8859-1`,
`Content-Transfer-Encoding: base64`,
'',
'VGhpcyBpcyBiYXNlNjQgZW5jb2RlZCB0ZXh0IGNvbnRlbnQu',
'',
`--${innerBoundary}`,
`Content-Type: application/json; charset=utf-8`,
'',
'{"message": "JSON MIME content", "test": true, "special": "àáâãäå"}',
'',
`--${innerBoundary}--`,
'',
`--${boundary}`,
`Content-Type: image/png`,
`Content-Disposition: attachment; filename="test.png"`,
`Content-Transfer-Encoding: base64`,
'',
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==',
'',
`--${boundary}`,
`Content-Type: text/csv`,
`Content-Disposition: attachment; filename="data.csv"`,
'',
'Name,Age,Email',
'John,25,john@example.com',
'Jane,30,jane@example.com',
'',
`--${boundary}`,
`Content-Type: application/pdf`,
`Content-Disposition: attachment; filename="document.pdf"`,
`Content-Transfer-Encoding: base64`,
'',
'JVBERi0xLjQKJcOkw7zDtsOVDQo=',
'',
`--${boundary}--`,
'.',
''
].join('\r\n');
console.log('Sending comprehensive MIME email with multiple parts and encodings');
socket.write(email);
step = 'sent';
dataBuffer = '';
} else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
if (!completed) {
completed = true;
console.log('Complex MIME message accepted successfully');
expect(true).toEqual(true);
socket.write('QUIT\r\n');
socket.end();
done.resolve();
}
}
});
socket.on('error', (err) => {
console.error('Socket error:', err);
done.reject(err);
});
await done.promise;
});
tap.test('MIME Handling - Quoted-printable encoding', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
let dataBuffer = '';
let step = 'greeting';
let completed = false;
socket.on('data', (data) => {
dataBuffer += data.toString();
console.log('Server response:', data.toString());
if (step === 'greeting' && dataBuffer.includes('220 ')) {
step = 'ehlo';
socket.write('EHLO testclient\r\n');
dataBuffer = '';
} else if (step === 'ehlo' && dataBuffer.includes('250')) {
step = 'mail';
socket.write('MAIL FROM:<sender@example.com>\r\n');
dataBuffer = '';
} else if (step === 'mail' && dataBuffer.includes('250')) {
step = 'rcpt';
socket.write('RCPT TO:<recipient@example.com>\r\n');
dataBuffer = '';
} else if (step === 'rcpt' && dataBuffer.includes('250')) {
step = 'data';
socket.write('DATA\r\n');
dataBuffer = '';
} else if (step === 'data' && dataBuffer.includes('354')) {
const email = [
`From: sender@example.com`,
`To: recipient@example.com`,
`Subject: =?UTF-8?Q?Quoted=2DPrintable=20Test=20=F0=9F=8C=9F?=`,
`Date: ${new Date().toUTCString()}`,
`Message-ID: <qp-test-${Date.now()}@example.com>`,
`MIME-Version: 1.0`,
`Content-Type: text/plain; charset=utf-8`,
`Content-Transfer-Encoding: quoted-printable`,
'',
'This is a test of quoted-printable encoding.',
'Special characters: =C3=A9 =C3=A8 =C3=AA =C3=AB',
'Long line that needs to be wrapped with soft line breaks at 76 character=',
's per line to comply with MIME standards for quoted-printable encoding.',
'Emoji: =F0=9F=98=80 =F0=9F=91=8D =F0=9F=8C=9F',
'.',
''
].join('\r\n');
socket.write(email);
step = 'sent';
dataBuffer = '';
} else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
if (!completed) {
completed = true;
console.log('Quoted-printable encoded email accepted');
expect(true).toEqual(true);
socket.write('QUIT\r\n');
socket.end();
done.resolve();
}
}
});
socket.on('error', (err) => {
console.error('Socket error:', err);
done.reject(err);
});
await done.promise;
});
tap.test('MIME Handling - Base64 encoding', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
let dataBuffer = '';
let step = 'greeting';
let completed = false;
socket.on('data', (data) => {
dataBuffer += data.toString();
console.log('Server response:', data.toString());
if (step === 'greeting' && dataBuffer.includes('220 ')) {
step = 'ehlo';
socket.write('EHLO testclient\r\n');
dataBuffer = '';
} else if (step === 'ehlo' && dataBuffer.includes('250')) {
step = 'mail';
socket.write('MAIL FROM:<sender@example.com>\r\n');
dataBuffer = '';
} else if (step === 'mail' && dataBuffer.includes('250')) {
step = 'rcpt';
socket.write('RCPT TO:<recipient@example.com>\r\n');
dataBuffer = '';
} else if (step === 'rcpt' && dataBuffer.includes('250')) {
step = 'data';
socket.write('DATA\r\n');
dataBuffer = '';
} else if (step === 'data' && dataBuffer.includes('354')) {
const boundary = 'base64-test-boundary';
const textContent = 'This is a test of base64 encoding with various content types.\nSpecial chars: éèêë\nEmoji: 😀 👍 🌟';
const base64Content = Buffer.from(textContent).toString('base64');
const email = [
`From: sender@example.com`,
`To: recipient@example.com`,
`Subject: Base64 Encoding Test`,
`Date: ${new Date().toUTCString()}`,
`Message-ID: <base64-test-${Date.now()}@example.com>`,
`MIME-Version: 1.0`,
`Content-Type: multipart/mixed; boundary="${boundary}"`,
'',
`--${boundary}`,
`Content-Type: text/plain; charset=utf-8`,
`Content-Transfer-Encoding: base64`,
'',
base64Content,
'',
`--${boundary}`,
`Content-Type: application/octet-stream`,
`Content-Disposition: attachment; filename="binary.dat"`,
`Content-Transfer-Encoding: base64`,
'',
'VGhpcyBpcyBiaW5hcnkgZGF0YSBmb3IgdGVzdGluZw==',
'',
`--${boundary}--`,
'.',
''
].join('\r\n');
socket.write(email);
step = 'sent';
dataBuffer = '';
} else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
if (!completed) {
completed = true;
console.log('Base64 encoded email accepted');
expect(true).toEqual(true);
socket.write('QUIT\r\n');
socket.end();
done.resolve();
}
}
});
socket.on('error', (err) => {
console.error('Socket error:', err);
done.reject(err);
});
await done.promise;
});
tap.test('MIME Handling - Content-Disposition headers', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
let dataBuffer = '';
let step = 'greeting';
let completed = false;
socket.on('data', (data) => {
dataBuffer += data.toString();
console.log('Server response:', data.toString());
if (step === 'greeting' && dataBuffer.includes('220 ')) {
step = 'ehlo';
socket.write('EHLO testclient\r\n');
dataBuffer = '';
} else if (step === 'ehlo' && dataBuffer.includes('250')) {
step = 'mail';
socket.write('MAIL FROM:<sender@example.com>\r\n');
dataBuffer = '';
} else if (step === 'mail' && dataBuffer.includes('250')) {
step = 'rcpt';
socket.write('RCPT TO:<recipient@example.com>\r\n');
dataBuffer = '';
} else if (step === 'rcpt' && dataBuffer.includes('250')) {
step = 'data';
socket.write('DATA\r\n');
dataBuffer = '';
} else if (step === 'data' && dataBuffer.includes('354')) {
const boundary = 'disposition-test-boundary';
const email = [
`From: sender@example.com`,
`To: recipient@example.com`,
`Subject: Content-Disposition Test`,
`Date: ${new Date().toUTCString()}`,
`Message-ID: <disposition-test-${Date.now()}@example.com>`,
`MIME-Version: 1.0`,
`Content-Type: multipart/mixed; boundary="${boundary}"`,
'',
`--${boundary}`,
`Content-Type: text/plain`,
`Content-Disposition: inline`,
'',
'This is inline text content.',
'',
`--${boundary}`,
`Content-Type: image/jpeg`,
`Content-Disposition: attachment; filename="photo.jpg"`,
`Content-Transfer-Encoding: base64`,
'',
'/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAEBAQ==',
'',
`--${boundary}`,
`Content-Type: application/pdf`,
`Content-Disposition: attachment; filename="report.pdf"; size=1234`,
`Content-Description: Monthly Report`,
'',
'PDF content here',
'',
`--${boundary}`,
`Content-Type: text/html`,
`Content-Disposition: inline; filename="content.html"`,
'',
'<html><body>Inline HTML content</body></html>',
'',
`--${boundary}--`,
'.',
''
].join('\r\n');
socket.write(email);
step = 'sent';
dataBuffer = '';
} else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
if (!completed) {
completed = true;
console.log('Email with various Content-Disposition headers accepted');
expect(true).toEqual(true);
socket.write('QUIT\r\n');
socket.end();
done.resolve();
}
}
});
socket.on('error', (err) => {
console.error('Socket error:', err);
done.reject(err);
});
await done.promise;
});
tap.test('MIME Handling - International character sets', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
let dataBuffer = '';
let step = 'greeting';
let completed = false;
socket.on('data', (data) => {
dataBuffer += data.toString();
console.log('Server response:', data.toString());
if (step === 'greeting' && dataBuffer.includes('220 ')) {
step = 'ehlo';
socket.write('EHLO testclient\r\n');
dataBuffer = '';
} else if (step === 'ehlo' && dataBuffer.includes('250')) {
step = 'mail';
socket.write('MAIL FROM:<sender@example.com>\r\n');
dataBuffer = '';
} else if (step === 'mail' && dataBuffer.includes('250')) {
step = 'rcpt';
socket.write('RCPT TO:<recipient@example.com>\r\n');
dataBuffer = '';
} else if (step === 'rcpt' && dataBuffer.includes('250')) {
step = 'data';
socket.write('DATA\r\n');
dataBuffer = '';
} else if (step === 'data' && dataBuffer.includes('354')) {
const boundary = 'intl-charset-boundary';
const email = [
`From: sender@example.com`,
`To: recipient@example.com`,
`Subject: International Character Sets`,
`Date: ${new Date().toUTCString()}`,
`Message-ID: <intl-charset-${Date.now()}@example.com>`,
`MIME-Version: 1.0`,
`Content-Type: multipart/mixed; boundary="${boundary}"`,
'',
`--${boundary}`,
`Content-Type: text/plain; charset=utf-8`,
'',
'UTF-8: Français, Español, Deutsch, 中文, 日本語, 한국어, العربية',
'',
`--${boundary}`,
`Content-Type: text/plain; charset=iso-8859-1`,
'',
'ISO-8859-1: Français, Español, Português',
'',
`--${boundary}`,
`Content-Type: text/plain; charset=windows-1252`,
'',
'Windows-1252: Special chars: €‚ƒ„…†‡',
'',
`--${boundary}`,
`Content-Type: text/plain; charset=shift_jis`,
'',
'Shift-JIS: Japanese text',
'',
`--${boundary}--`,
'.',
''
].join('\r\n');
socket.write(email);
step = 'sent';
dataBuffer = '';
} else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
if (!completed) {
completed = true;
console.log('Email with international character sets accepted');
expect(true).toEqual(true);
socket.write('QUIT\r\n');
socket.end();
done.resolve();
}
}
});
socket.on('error', (err) => {
console.error('Socket error:', err);
done.reject(err);
});
await done.promise;
});
tap.test('cleanup - stop test server', async () => {
await stopTestServer(testServer);
});
tap.start();

View File

@ -0,0 +1,629 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as net from 'net';
import * as fs from 'fs';
import * as path from 'path';
import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'
import type { ITestServer } from '../../helpers/server.loader.js';
const TEST_PORT = 2525;
const SAMPLE_FILES_DIR = path.join(process.cwd(), '.nogit', 'sample-files');
let testServer: ITestServer;
// Helper function to read and encode files
function readFileAsBase64(filePath: string): string {
try {
const fileContent = fs.readFileSync(filePath);
return fileContent.toString('base64');
} catch (err) {
console.error(`Failed to read file ${filePath}:`, err);
return '';
}
}
tap.test('setup - start test server', async () => {
testServer = await startTestServer({ port: TEST_PORT });
await new Promise(resolve => setTimeout(resolve, 1000));
});
tap.test('Attachment Handling - Multiple file types', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
let dataBuffer = '';
let step = 'greeting';
let completed = false;
socket.on('data', (data) => {
if (completed) return;
dataBuffer += data.toString();
console.log('Server response:', data.toString());
if (step === 'greeting' && dataBuffer.includes('220 ')) {
step = 'ehlo';
socket.write('EHLO testclient\r\n');
dataBuffer = '';
} else if (step === 'ehlo' && dataBuffer.includes('250')) {
step = 'mail';
socket.write('MAIL FROM:<sender@example.com>\r\n');
dataBuffer = '';
} else if (step === 'mail' && dataBuffer.includes('250')) {
step = 'rcpt';
socket.write('RCPT TO:<recipient@example.com>\r\n');
dataBuffer = '';
} else if (step === 'rcpt' && dataBuffer.includes('250')) {
step = 'data';
socket.write('DATA\r\n');
dataBuffer = '';
} else if (step === 'data' && dataBuffer.includes('354')) {
const boundary = 'attachment-test-boundary-12345';
// Create various attachments
const textAttachment = 'This is a text attachment content.\nIt has multiple lines.\nAnd special chars: åäö';
const jsonAttachment = JSON.stringify({
name: 'test',
data: [1, 2, 3],
unicode: 'ñoño',
special: '∑∆≈'
}, null, 2);
// Read real files from sample directory
const sampleImage = readFileAsBase64(path.join(SAMPLE_FILES_DIR, '003-pdflatex-image/image.jpg'));
const minimalPdf = readFileAsBase64(path.join(SAMPLE_FILES_DIR, '001-trivial/minimal-document.pdf'));
const multiPagePdf = readFileAsBase64(path.join(SAMPLE_FILES_DIR, '004-pdflatex-4-pages/pdflatex-4-pages.pdf'));
const pdfWithAttachment = readFileAsBase64(path.join(SAMPLE_FILES_DIR, '025-attachment/with-attachment.pdf'));
const email = [
`From: sender@example.com`,
`To: recipient@example.com`,
`Subject: Attachment Handling Test - Multiple Types`,
`Date: ${new Date().toUTCString()}`,
`Message-ID: <attachment-test-${Date.now()}@example.com>`,
`MIME-Version: 1.0`,
`Content-Type: multipart/mixed; boundary="${boundary}"`,
'',
'This is a multi-part message with various attachments.',
'',
`--${boundary}`,
`Content-Type: text/plain; charset=utf-8`,
'',
'This email tests attachment handling capabilities.',
'The server should properly process all attached files.',
'',
`--${boundary}`,
`Content-Type: text/plain; charset=utf-8`,
`Content-Disposition: attachment; filename="document.txt"`,
`Content-Transfer-Encoding: 7bit`,
'',
textAttachment,
'',
`--${boundary}`,
`Content-Type: application/json; charset=utf-8`,
`Content-Disposition: attachment; filename="data.json"`,
'',
jsonAttachment,
'',
`--${boundary}`,
`Content-Type: image/jpeg`,
`Content-Disposition: attachment; filename="sample-image.jpg"`,
`Content-Transfer-Encoding: base64`,
'',
sampleImage,
'',
`--${boundary}`,
`Content-Type: application/octet-stream`,
`Content-Disposition: attachment; filename="binary.bin"`,
`Content-Transfer-Encoding: base64`,
'',
Buffer.from('Binary file content with null bytes\0\0\0').toString('base64'),
'',
`--${boundary}`,
`Content-Type: text/csv`,
`Content-Disposition: attachment; filename="spreadsheet.csv"`,
'',
'Name,Age,Country',
'Alice,25,Sweden',
'Bob,30,Norway',
'Charlie,35,Denmark',
'',
`--${boundary}`,
`Content-Type: application/xml; charset=utf-8`,
`Content-Disposition: attachment; filename="config.xml"`,
'',
'<?xml version="1.0" encoding="UTF-8"?>',
'<config>',
' <setting name="test">value</setting>',
' <unicode>ñoño ∑∆≈</unicode>',
'</config>',
'',
`--${boundary}`,
`Content-Type: application/pdf`,
`Content-Disposition: attachment; filename="minimal-document.pdf"`,
`Content-Transfer-Encoding: base64`,
'',
minimalPdf,
'',
`--${boundary}`,
`Content-Type: application/pdf`,
`Content-Disposition: attachment; filename="multi-page-document.pdf"`,
`Content-Transfer-Encoding: base64`,
'',
multiPagePdf,
'',
`--${boundary}`,
`Content-Type: application/pdf`,
`Content-Disposition: attachment; filename="pdf-with-embedded-attachment.pdf"`,
`Content-Transfer-Encoding: base64`,
'',
pdfWithAttachment,
'',
`--${boundary}`,
`Content-Type: text/html; charset=utf-8`,
`Content-Disposition: attachment; filename="webpage.html"`,
'',
'<!DOCTYPE html>',
'<html><head><title>Test</title></head>',
'<body><h1>HTML Attachment</h1><p>Content with <em>markup</em></p></body>',
'</html>',
'',
`--${boundary}--`,
'.',
''
].join('\r\n');
console.log('Sending email with 10 different attachment types including real PDFs');
socket.write(email);
dataBuffer = '';
step = 'sent';
} else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
if (!completed) {
completed = true;
console.log('Email with multiple attachments accepted successfully');
expect(true).toEqual(true);
socket.write('QUIT\r\n');
socket.end();
done.resolve();
}
}
});
socket.on('error', (err) => {
console.error('Socket error:', err);
done.reject(err);
});
await done.promise;
});
tap.test('Attachment Handling - Large attachment', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
let dataBuffer = '';
let step = 'greeting';
let completed = false;
socket.on('data', (data) => {
if (completed) return;
dataBuffer += data.toString();
console.log('Server response:', data.toString());
if (step === 'greeting' && dataBuffer.includes('220 ')) {
step = 'ehlo';
socket.write('EHLO testclient\r\n');
dataBuffer = '';
} else if (step === 'ehlo' && dataBuffer.includes('250')) {
step = 'mail';
socket.write('MAIL FROM:<sender@example.com>\r\n');
dataBuffer = '';
} else if (step === 'mail' && dataBuffer.includes('250')) {
step = 'rcpt';
socket.write('RCPT TO:<recipient@example.com>\r\n');
dataBuffer = '';
} else if (step === 'rcpt' && dataBuffer.includes('250')) {
step = 'data';
socket.write('DATA\r\n');
dataBuffer = '';
} else if (step === 'data' && dataBuffer.includes('354')) {
const boundary = 'large-attachment-boundary';
// Use a real large PDF file
const largePdf = readFileAsBase64(path.join(SAMPLE_FILES_DIR, '009-pdflatex-geotopo/GeoTopo.pdf'));
const largePdfSize = Buffer.from(largePdf, 'base64').length;
console.log(`Large PDF size: ${(largePdfSize / 1024).toFixed(2)}KB`);
const email = [
`From: sender@example.com`,
`To: recipient@example.com`,
`Subject: Large Attachment Test`,
`Date: ${new Date().toUTCString()}`,
`Message-ID: <large-attach-${Date.now()}@example.com>`,
`MIME-Version: 1.0`,
`Content-Type: multipart/mixed; boundary="${boundary}"`,
'',
`--${boundary}`,
`Content-Type: text/plain`,
'',
'This email contains a large attachment.',
'',
`--${boundary}`,
`Content-Type: application/pdf`,
`Content-Disposition: attachment; filename="large-geotopo.pdf"`,
`Content-Transfer-Encoding: base64`,
'',
largePdf,
'',
`--${boundary}--`,
'.',
''
].join('\r\n');
console.log(`Sending email with large PDF attachment (${(largePdfSize / 1024).toFixed(2)}KB)`);
socket.write(email);
dataBuffer = '';
step = 'sent';
} else if (step === 'sent' && (dataBuffer.includes('250 ') || dataBuffer.includes('552 '))) {
if (!completed) {
completed = true;
const accepted = dataBuffer.includes('250');
const rejected = dataBuffer.includes('552'); // Size exceeded
console.log(`Large attachment: ${accepted ? 'accepted' : 'rejected (size limit)'}`);
expect(accepted || rejected).toEqual(true);
socket.write('QUIT\r\n');
socket.end();
done.resolve();
}
}
});
socket.on('error', (err) => {
console.error('Socket error:', err);
done.reject(err);
});
await done.promise;
});
tap.test('Attachment Handling - Inline vs attachment disposition', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
let dataBuffer = '';
let step = 'greeting';
let completed = false;
socket.on('data', (data) => {
if (completed) return;
dataBuffer += data.toString();
console.log('Server response:', data.toString());
if (step === 'greeting' && dataBuffer.includes('220 ')) {
step = 'ehlo';
socket.write('EHLO testclient\r\n');
dataBuffer = '';
} else if (step === 'ehlo' && dataBuffer.includes('250')) {
step = 'mail';
socket.write('MAIL FROM:<sender@example.com>\r\n');
dataBuffer = '';
} else if (step === 'mail' && dataBuffer.includes('250')) {
step = 'rcpt';
socket.write('RCPT TO:<recipient@example.com>\r\n');
dataBuffer = '';
} else if (step === 'rcpt' && dataBuffer.includes('250')) {
step = 'data';
socket.write('DATA\r\n');
dataBuffer = '';
} else if (step === 'data' && dataBuffer.includes('354')) {
const boundary = 'inline-attachment-boundary';
const email = [
`From: sender@example.com`,
`To: recipient@example.com`,
`Subject: Inline vs Attachment Test`,
`Date: ${new Date().toUTCString()}`,
`Message-ID: <inline-test-${Date.now()}@example.com>`,
`MIME-Version: 1.0`,
`Content-Type: multipart/related; boundary="${boundary}"`,
'',
`--${boundary}`,
`Content-Type: text/html`,
'',
'<html><body>',
'<p>This email has inline images:</p>',
'<img src="cid:image1">',
'<img src="cid:image2">',
'</body></html>',
'',
`--${boundary}`,
`Content-Type: image/png`,
`Content-ID: <image1>`,
`Content-Disposition: inline; filename="inline1.png"`,
`Content-Transfer-Encoding: base64`,
'',
readFileAsBase64(path.join(SAMPLE_FILES_DIR, '008-reportlab-inline-image/smile.png')) || 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
'',
`--${boundary}`,
`Content-Type: image/png`,
`Content-ID: <image2>`,
`Content-Disposition: inline; filename="inline2.png"`,
`Content-Transfer-Encoding: base64`,
'',
readFileAsBase64(path.join(SAMPLE_FILES_DIR, '019-grayscale-image/page-0-X0.png')) || 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==',
'',
`--${boundary}`,
`Content-Type: application/pdf`,
`Content-Disposition: attachment; filename="document.pdf"`,
`Content-Transfer-Encoding: base64`,
'',
readFileAsBase64(path.join(SAMPLE_FILES_DIR, '013-reportlab-overlay/reportlab-overlay.pdf')) || 'JVBERi0xLjQKJcOkw7zDtsOVDQo=',
'',
`--${boundary}--`,
'.',
''
].join('\r\n');
socket.write(email);
dataBuffer = '';
step = 'sent';
} else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
if (!completed) {
completed = true;
console.log('Email with inline and attachment dispositions accepted');
expect(true).toEqual(true);
socket.write('QUIT\r\n');
socket.end();
done.resolve();
}
}
});
socket.on('error', (err) => {
console.error('Socket error:', err);
done.reject(err);
});
await done.promise;
});
tap.test('Attachment Handling - Filename encoding', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
let dataBuffer = '';
let step = 'greeting';
let completed = false;
socket.on('data', (data) => {
if (completed) return;
dataBuffer += data.toString();
console.log('Server response:', data.toString());
if (step === 'greeting' && dataBuffer.includes('220 ')) {
step = 'ehlo';
socket.write('EHLO testclient\r\n');
dataBuffer = '';
} else if (step === 'ehlo' && dataBuffer.includes('250')) {
step = 'mail';
socket.write('MAIL FROM:<sender@example.com>\r\n');
dataBuffer = '';
} else if (step === 'mail' && dataBuffer.includes('250')) {
step = 'rcpt';
socket.write('RCPT TO:<recipient@example.com>\r\n');
dataBuffer = '';
} else if (step === 'rcpt' && dataBuffer.includes('250')) {
step = 'data';
socket.write('DATA\r\n');
dataBuffer = '';
} else if (step === 'data' && dataBuffer.includes('354')) {
const boundary = 'filename-encoding-boundary';
const email = [
`From: sender@example.com`,
`To: recipient@example.com`,
`Subject: Filename Encoding Test`,
`Date: ${new Date().toUTCString()}`,
`Message-ID: <filename-test-${Date.now()}@example.com>`,
`MIME-Version: 1.0`,
`Content-Type: multipart/mixed; boundary="${boundary}"`,
'',
`--${boundary}`,
`Content-Type: text/plain`,
'',
'Testing various filename encodings.',
'',
`--${boundary}`,
`Content-Type: text/plain`,
`Content-Disposition: attachment; filename="simple.txt"`,
'',
'Simple ASCII filename',
'',
`--${boundary}`,
`Content-Type: text/plain`,
`Content-Disposition: attachment; filename="åäö-nordic.txt"`,
'',
'Nordic characters in filename',
'',
`--${boundary}`,
`Content-Type: text/plain`,
`Content-Disposition: attachment; filename*=UTF-8''%C3%A5%C3%A4%C3%B6-encoded.txt`,
'',
'RFC 2231 encoded filename',
'',
`--${boundary}`,
`Content-Type: text/plain`,
`Content-Disposition: attachment; filename="=?UTF-8?B?8J+YgC1lbW9qaS50eHQ=?="`,
'',
'MIME encoded filename with emoji',
'',
`--${boundary}`,
`Content-Type: text/plain`,
`Content-Disposition: attachment; filename="very long filename that exceeds normal limits and should be handled properly by the server.txt"`,
'',
'Very long filename',
'',
`--${boundary}--`,
'.',
''
].join('\r\n');
socket.write(email);
dataBuffer = '';
step = 'sent';
} else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
if (!completed) {
completed = true;
console.log('Email with various filename encodings accepted');
expect(true).toEqual(true);
socket.write('QUIT\r\n');
socket.end();
done.resolve();
}
}
});
socket.on('error', (err) => {
console.error('Socket error:', err);
done.reject(err);
});
await done.promise;
});
tap.test('Attachment Handling - Empty and malformed attachments', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
let dataBuffer = '';
let step = 'greeting';
let completed = false;
socket.on('data', (data) => {
if (completed) return;
dataBuffer += data.toString();
console.log('Server response:', data.toString());
if (step === 'greeting' && dataBuffer.includes('220 ')) {
step = 'ehlo';
socket.write('EHLO testclient\r\n');
dataBuffer = '';
} else if (step === 'ehlo' && dataBuffer.includes('250')) {
step = 'mail';
socket.write('MAIL FROM:<sender@example.com>\r\n');
dataBuffer = '';
} else if (step === 'mail' && dataBuffer.includes('250')) {
step = 'rcpt';
socket.write('RCPT TO:<recipient@example.com>\r\n');
dataBuffer = '';
} else if (step === 'rcpt' && dataBuffer.includes('250')) {
step = 'data';
socket.write('DATA\r\n');
dataBuffer = '';
} else if (step === 'data' && dataBuffer.includes('354')) {
const boundary = 'malformed-boundary';
const email = [
`From: sender@example.com`,
`To: recipient@example.com`,
`Subject: Empty and Malformed Attachments`,
`Date: ${new Date().toUTCString()}`,
`Message-ID: <malformed-${Date.now()}@example.com>`,
`MIME-Version: 1.0`,
`Content-Type: multipart/mixed; boundary="${boundary}"`,
'',
`--${boundary}`,
`Content-Type: text/plain`,
'',
'Testing empty and malformed attachments.',
'',
`--${boundary}`,
`Content-Type: application/octet-stream`,
`Content-Disposition: attachment; filename="empty.dat"`,
'',
'', // Empty attachment
`--${boundary}`,
`Content-Type: text/plain`,
`Content-Disposition: attachment`, // Missing filename
'',
'Attachment without filename',
'',
`--${boundary}`,
`Content-Type: application/pdf`,
`Content-Disposition: attachment; filename="broken.pdf"`,
`Content-Transfer-Encoding: base64`,
'',
'NOT-VALID-BASE64-@#$%', // Invalid base64
'',
`--${boundary}`,
`Content-Disposition: attachment; filename="no-content-type.txt"`, // Missing Content-Type
'',
'Attachment without Content-Type header',
'',
`--${boundary}--`,
'.',
''
].join('\r\n');
socket.write(email);
dataBuffer = '';
step = 'sent';
} else if (step === 'sent' && (dataBuffer.includes('250 ') || dataBuffer.includes('550 '))) {
if (!completed) {
completed = true;
const result = dataBuffer.includes('250') ? 'accepted' : 'rejected';
console.log(`Email with malformed attachments ${result}`);
expect(true).toEqual(true);
socket.write('QUIT\r\n');
socket.end();
done.resolve();
}
}
});
socket.on('error', (err) => {
console.error('Socket error:', err);
done.reject(err);
});
await done.promise;
});
tap.test('cleanup - stop test server', async () => {
await stopTestServer(testServer);
});
tap.start();

View File

@ -0,0 +1,462 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as net from 'net';
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
const TEST_PORT = 30050;
let testServer: ITestServer;
tap.test('setup - start test server', async () => {
testServer = await startTestServer({ port: TEST_PORT, hostname: 'localhost' });
expect(testServer).toBeDefined();
});
tap.test('Special Character Handling - Comprehensive Unicode test', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
let dataBuffer = '';
let step = 'greeting';
let completed = false;
socket.on('data', (data) => {
dataBuffer += data.toString();
console.log('Server response:', data.toString());
if (step === 'greeting' && dataBuffer.includes('220 ')) {
step = 'ehlo';
socket.write('EHLO testclient\r\n');
dataBuffer = '';
} else if (step === 'ehlo' && dataBuffer.includes('250')) {
step = 'mail';
socket.write('MAIL FROM:<sender@example.com>\r\n');
dataBuffer = '';
} else if (step === 'mail' && dataBuffer.includes('250')) {
step = 'rcpt';
socket.write('RCPT TO:<recipient@example.com>\r\n');
dataBuffer = '';
} else if (step === 'rcpt' && dataBuffer.includes('250')) {
step = 'data';
socket.write('DATA\r\n');
dataBuffer = '';
} else if (step === 'data' && dataBuffer.includes('354')) {
const email = [
`From: sender@example.com`,
`To: recipient@example.com`,
`Subject: Special Character Test - Unicode & Symbols ñáéíóú`,
`Date: ${new Date().toUTCString()}`,
`Message-ID: <special-chars-${Date.now()}@example.com>`,
`MIME-Version: 1.0`,
`Content-Type: text/plain; charset=utf-8`,
`Content-Transfer-Encoding: 8bit`,
'',
'This email tests special character handling:',
'',
'=== UNICODE CHARACTERS ===',
'Accented letters: àáâãäåæçèéêëìíîïñòóôõöøùúûüý',
'German umlauts: äöüÄÖÜß',
'Scandinavian: åäöÅÄÖ',
'French: àâéèêëïîôœùûüÿç',
'Spanish: ñáéíóúü¿¡',
'Polish: ąćęłńóśźż',
'Russian: абвгдеёжзийклмнопрстуфхцчшщъыьэюя',
'Greek: αβγδεζηθικλμνξοπρστυφχψω',
'Arabic: العربية',
'Hebrew: עברית',
'Chinese: 中文测试',
'Japanese: 日本語テスト',
'Korean: 한국어 테스트',
'Thai: ภาษาไทย',
'',
'=== MATHEMATICAL SYMBOLS ===',
'Math: ∑∏∫∆∇∂∞±×÷≠≤≥≈∝∪∩⊂⊃∈∀∃',
'Greek letters: αβγδεζηθικλμνξοπρστυφχψω',
'Arrows: ←→↑↓↔↕⇐⇒⇑⇓⇔⇕',
'',
'=== CURRENCY & SYMBOLS ===',
'Currency: $€£¥¢₹₽₩₪₫₨₦₡₵₴₸₼₲₱',
'Symbols: ©®™§¶†‡•…‰‱°℃℉№',
`Punctuation: «»""''‚„‹›–—―‖‗''""‚„…‰′″‴‵‶‷‸‹›※‼‽⁇⁈⁉⁏⁐⁑⁒⁓⁔⁕⁖⁗⁘⁙⁚⁛⁜⁝⁞`,
'',
'=== EMOJI & SYMBOLS ===',
'Common: ☀☁☂☃☄★☆☇☈☉☊☋☌☍☎☏☐☑☒☓☔☕☖☗☘☙☚☛☜☝☞☟☠☡☢☣☤☥☦☧☨☩☪☫☬☭☮☯☰☱☲☳☴☵☶☷',
'Smileys: ☺☻☹☿♀♁♂♃♄♅♆♇',
'Hearts: ♡♢♣♤♥♦♧♨♩♪♫♬♭♮♯',
'',
'=== SPECIAL FORMATTING ===',
'Zero-width chars: ',
'Combining: e̊åa̋o̧ç',
'Ligatures: fffiflffifflſtst',
'Fractions: ½⅓⅔¼¾⅛⅜⅝⅞',
'Superscript: ⁰¹²³⁴⁵⁶⁷⁸⁹',
'Subscript: ₀₁₂₃₄₅₆₇₈₉',
'',
'End of special character test.',
'.',
''
].join('\r\n');
console.log('Sending email with comprehensive Unicode characters');
socket.write(email);
step = 'sent';
dataBuffer = '';
} else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
if (!completed) {
completed = true;
console.log('Email with special characters accepted successfully');
expect(true).toEqual(true);
socket.write('QUIT\r\n');
socket.end();
done.resolve();
}
}
});
socket.on('error', (err) => {
console.error('Socket error:', err);
done.reject(err);
});
await done.promise;
});
tap.test('Special Character Handling - Control characters', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
let dataBuffer = '';
let step = 'greeting';
let completed = false;
socket.on('data', (data) => {
dataBuffer += data.toString();
console.log('Server response:', data.toString());
if (step === 'greeting' && dataBuffer.includes('220 ')) {
step = 'ehlo';
socket.write('EHLO testclient\r\n');
dataBuffer = '';
} else if (step === 'ehlo' && dataBuffer.includes('250')) {
step = 'mail';
socket.write('MAIL FROM:<sender@example.com>\r\n');
dataBuffer = '';
} else if (step === 'mail' && dataBuffer.includes('250')) {
step = 'rcpt';
socket.write('RCPT TO:<recipient@example.com>\r\n');
dataBuffer = '';
} else if (step === 'rcpt' && dataBuffer.includes('250')) {
step = 'data';
socket.write('DATA\r\n');
dataBuffer = '';
} else if (step === 'data' && dataBuffer.includes('354')) {
const email = [
`From: sender@example.com`,
`To: recipient@example.com`,
`Subject: Control Character Test`,
`Date: ${new Date().toUTCString()}`,
`Message-ID: <control-chars-${Date.now()}@example.com>`,
`MIME-Version: 1.0`,
`Content-Type: text/plain; charset=utf-8`,
'',
'=== CONTROL CHARACTERS TEST ===',
'Tab character: (between words)',
'Non-breaking space: word word',
'Soft hyphen: super­cali­fragi­listic­expi­ali­docious',
'Vertical tab: word\x0Bword',
'Form feed: word\x0Cword',
'Backspace: word\x08word',
'',
'=== LINE ENDING TESTS ===',
'Unix LF: Line1\nLine2',
'Windows CRLF: Line3\r\nLine4',
'Mac CR: Line5\rLine6',
'',
'=== BOUNDARY CHARACTERS ===',
'SMTP boundary test: . (dot at start)',
'Double dots: .. (escaped in SMTP)',
'CRLF.CRLF sequence test',
'.',
''
].join('\r\n');
socket.write(email);
step = 'sent';
dataBuffer = '';
} else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
if (!completed) {
completed = true;
console.log('Email with control characters accepted');
expect(true).toEqual(true);
socket.write('QUIT\r\n');
socket.end();
done.resolve();
}
}
});
socket.on('error', (err) => {
console.error('Socket error:', err);
done.reject(err);
});
await done.promise;
});
tap.test('Special Character Handling - Subject header encoding', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
let dataBuffer = '';
let step = 'greeting';
let completed = false;
socket.on('data', (data) => {
dataBuffer += data.toString();
console.log('Server response:', data.toString());
if (step === 'greeting' && dataBuffer.includes('220 ')) {
step = 'ehlo';
socket.write('EHLO testclient\r\n');
dataBuffer = '';
} else if (step === 'ehlo' && dataBuffer.includes('250')) {
step = 'mail';
socket.write('MAIL FROM:<sender@example.com>\r\n');
dataBuffer = '';
} else if (step === 'mail' && dataBuffer.includes('250')) {
step = 'rcpt';
socket.write('RCPT TO:<recipient@example.com>\r\n');
dataBuffer = '';
} else if (step === 'rcpt' && dataBuffer.includes('250')) {
step = 'data';
socket.write('DATA\r\n');
dataBuffer = '';
} else if (step === 'data' && dataBuffer.includes('354')) {
const email = [
`From: sender@example.com`,
`To: recipient@example.com`,
`Subject: =?UTF-8?B?8J+YgCBFbW9qaSBpbiBTdWJqZWN0IOKcqCDwn4yI?=`,
`Subject: =?UTF-8?Q?Quoted=2DPrintable=20Subject=20=C3=A1=C3=A9=C3=AD=C3=B3=C3=BA?=`,
`Date: ${new Date().toUTCString()}`,
`Message-ID: <encoded-subject-${Date.now()}@example.com>`,
'',
'Testing encoded subject headers with special characters.',
'.',
''
].join('\r\n');
socket.write(email);
step = 'sent';
dataBuffer = '';
} else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
if (!completed) {
completed = true;
console.log('Email with encoded subject headers accepted');
expect(true).toEqual(true);
socket.write('QUIT\r\n');
socket.end();
done.resolve();
}
}
});
socket.on('error', (err) => {
console.error('Socket error:', err);
done.reject(err);
});
await done.promise;
});
tap.test('Special Character Handling - Address headers with special chars', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
let dataBuffer = '';
let step = 'greeting';
let completed = false;
socket.on('data', (data) => {
dataBuffer += data.toString();
console.log('Server response:', data.toString());
if (step === 'greeting' && dataBuffer.includes('220 ')) {
step = 'ehlo';
socket.write('EHLO testclient\r\n');
dataBuffer = '';
} else if (step === 'ehlo' && dataBuffer.includes('250')) {
step = 'mail';
socket.write('MAIL FROM:<sender@example.com>\r\n');
dataBuffer = '';
} else if (step === 'mail' && dataBuffer.includes('250')) {
step = 'rcpt';
socket.write('RCPT TO:<recipient@example.com>\r\n');
dataBuffer = '';
} else if (step === 'rcpt' && dataBuffer.includes('250')) {
step = 'data';
socket.write('DATA\r\n');
dataBuffer = '';
} else if (step === 'data' && dataBuffer.includes('354')) {
const email = [
`From: "José García" <jose@example.com>`,
`To: "François Müller" <francois@example.com>, "北京用户" <beijing@example.com>`,
`Cc: =?UTF-8?B?IkFubmEgw4XDpMO2Ig==?= <anna@example.com>`,
`Reply-To: "Søren Ñoño" <soren@example.com>`,
`Subject: Special names in address headers`,
`Date: ${new Date().toUTCString()}`,
`Message-ID: <special-addrs-${Date.now()}@example.com>`,
'',
'Testing special characters in email addresses and display names.',
'.',
''
].join('\r\n');
socket.write(email);
step = 'sent';
dataBuffer = '';
} else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
if (!completed) {
completed = true;
console.log('Email with special characters in addresses accepted');
expect(true).toEqual(true);
socket.write('QUIT\r\n');
socket.end();
done.resolve();
}
}
});
socket.on('error', (err) => {
console.error('Socket error:', err);
done.reject(err);
});
await done.promise;
});
tap.test('Special Character Handling - Mixed encodings', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
let dataBuffer = '';
let step = 'greeting';
let completed = false;
socket.on('data', (data) => {
dataBuffer += data.toString();
console.log('Server response:', data.toString());
if (step === 'greeting' && dataBuffer.includes('220 ')) {
step = 'ehlo';
socket.write('EHLO testclient\r\n');
dataBuffer = '';
} else if (step === 'ehlo' && dataBuffer.includes('250')) {
step = 'mail';
socket.write('MAIL FROM:<sender@example.com>\r\n');
dataBuffer = '';
} else if (step === 'mail' && dataBuffer.includes('250')) {
step = 'rcpt';
socket.write('RCPT TO:<recipient@example.com>\r\n');
dataBuffer = '';
} else if (step === 'rcpt' && dataBuffer.includes('250')) {
step = 'data';
socket.write('DATA\r\n');
dataBuffer = '';
} else if (step === 'data' && dataBuffer.includes('354')) {
const boundary = 'mixed-encoding-boundary';
const email = [
`From: sender@example.com`,
`To: recipient@example.com`,
`Subject: Mixed Encoding Test`,
`Date: ${new Date().toUTCString()}`,
`Message-ID: <mixed-enc-${Date.now()}@example.com>`,
`MIME-Version: 1.0`,
`Content-Type: multipart/mixed; boundary="${boundary}"`,
'',
`--${boundary}`,
`Content-Type: text/plain; charset=utf-8`,
`Content-Transfer-Encoding: 8bit`,
'',
'UTF-8 part: ñáéíóú 中文 日本語',
'',
`--${boundary}`,
`Content-Type: text/plain; charset=iso-8859-1`,
`Content-Transfer-Encoding: quoted-printable`,
'',
'ISO-8859-1 part: =F1=E1=E9=ED=F3=FA',
'',
`--${boundary}`,
`Content-Type: text/plain; charset=windows-1252`,
'',
'Windows-1252 part: €‚ƒ„…†‡',
'',
`--${boundary}`,
`Content-Type: text/plain; charset=utf-16`,
`Content-Transfer-Encoding: base64`,
'',
Buffer.from('UTF-16 text: ñoño', 'utf16le').toString('base64'),
'',
`--${boundary}--`,
'.',
''
].join('\r\n');
socket.write(email);
step = 'sent';
dataBuffer = '';
} else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
if (!completed) {
completed = true;
console.log('Email with mixed character encodings accepted');
expect(true).toEqual(true);
socket.write('QUIT\r\n');
socket.end();
done.resolve();
}
}
});
socket.on('error', (err) => {
console.error('Socket error:', err);
done.reject(err);
});
await done.promise;
});
tap.test('cleanup - stop test server', async () => {
await stopTestServer(testServer);
expect(true).toEqual(true);
});
tap.start();

View File

@ -0,0 +1,527 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as net from 'net';
import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'
import type { ITestServer } from '../../helpers/server.loader.js';
const TEST_PORT = 2525;
let testServer: ITestServer;
tap.test('setup - start test server', async () => {
testServer = await startTestServer({ port: TEST_PORT });
await new Promise(resolve => setTimeout(resolve, 1000));
});
tap.test('Email Routing - Local domain routing', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
let dataBuffer = '';
let step = 'greeting';
let completed = false;
socket.on('data', (data) => {
if (completed) return;
dataBuffer += data.toString();
console.log('Server response:', data.toString());
if (step === 'greeting' && dataBuffer.includes('220 ')) {
step = 'ehlo';
socket.write('EHLO localhost\r\n');
dataBuffer = '';
} else if (step === 'ehlo' && dataBuffer.includes('250')) {
step = 'mail';
// Local sender
socket.write('MAIL FROM:<test@example.com>\r\n');
dataBuffer = '';
} else if (step === 'mail' && dataBuffer.includes('250')) {
step = 'rcpt';
// Local recipient
socket.write('RCPT TO:<local@localhost>\r\n');
dataBuffer = '';
} else if (step === 'rcpt') {
const accepted = dataBuffer.includes('250');
console.log(`Local domain routing: ${accepted ? 'accepted' : 'rejected'}`);
if (accepted) {
step = 'data';
socket.write('DATA\r\n');
dataBuffer = '';
} else {
socket.write('QUIT\r\n');
socket.end();
done.resolve();
}
} else if (step === 'data' && dataBuffer.includes('354')) {
const email = [
`From: test@example.com`,
`To: local@localhost`,
`Subject: Local Domain Routing Test`,
`Date: ${new Date().toUTCString()}`,
`Message-ID: <local-routing-${Date.now()}@localhost>`,
'',
'This email tests local domain routing.',
'The server should route this email locally.',
'.',
''
].join('\r\n');
socket.write(email);
dataBuffer = '';
step = 'sent';
} else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
if (!completed) {
completed = true;
console.log('Local domain email routed successfully');
expect(true).toEqual(true);
socket.write('QUIT\r\n');
socket.end();
done.resolve();
}
}
});
socket.on('error', (err) => {
console.error('Socket error:', err);
done.reject(err);
});
await done.promise;
});
tap.test('Email Routing - External domain routing', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
let dataBuffer = '';
let step = 'greeting';
let completed = false;
socket.on('data', (data) => {
if (completed) return;
dataBuffer += data.toString();
console.log('Server response:', data.toString());
if (step === 'greeting' && dataBuffer.includes('220 ')) {
step = 'ehlo';
socket.write('EHLO localhost\r\n');
dataBuffer = '';
} else if (step === 'ehlo' && dataBuffer.includes('250')) {
step = 'mail';
socket.write('MAIL FROM:<sender@example.com>\r\n');
dataBuffer = '';
} else if (step === 'mail' && dataBuffer.includes('250')) {
step = 'rcpt';
// External recipient
socket.write('RCPT TO:<recipient@external.com>\r\n');
dataBuffer = '';
} else if (step === 'rcpt') {
const accepted = dataBuffer.includes('250');
console.log(`External domain routing: ${accepted ? 'accepted' : 'rejected'}`);
if (accepted) {
step = 'data';
socket.write('DATA\r\n');
dataBuffer = '';
} else {
socket.write('QUIT\r\n');
socket.end();
done.resolve();
}
} else if (step === 'data' && dataBuffer.includes('354')) {
const email = [
`From: sender@example.com`,
`To: recipient@external.com`,
`Subject: External Domain Routing Test`,
`Date: ${new Date().toUTCString()}`,
`Message-ID: <external-routing-${Date.now()}@example.com>`,
'',
'This email tests external domain routing.',
'The server should accept this for relay.',
'.',
''
].join('\r\n');
socket.write(email);
dataBuffer = '';
step = 'sent';
} else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
if (!completed) {
completed = true;
console.log('External domain email accepted for relay');
expect(true).toEqual(true);
socket.write('QUIT\r\n');
socket.end();
done.resolve();
}
}
});
socket.on('error', (err) => {
console.error('Socket error:', err);
done.reject(err);
});
await done.promise;
});
tap.test('Email Routing - Multiple recipients', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
let dataBuffer = '';
let step = 'greeting';
let recipientCount = 0;
const totalRecipients = 5;
let completed = false;
socket.on('data', (data) => {
if (completed) return;
dataBuffer += data.toString();
console.log('Server response:', data.toString());
if (step === 'greeting' && dataBuffer.includes('220 ')) {
step = 'ehlo';
socket.write('EHLO localhost\r\n');
dataBuffer = '';
} else if (step === 'ehlo' && dataBuffer.includes('250')) {
step = 'mail';
socket.write('MAIL FROM:<sender@example.com>\r\n');
dataBuffer = '';
} else if (step === 'mail' && dataBuffer.includes('250')) {
step = 'rcpt';
recipientCount++;
socket.write(`RCPT TO:<recipient${recipientCount}@example.com>\r\n`);
dataBuffer = '';
} else if (step === 'rcpt' && dataBuffer.includes('250')) {
if (recipientCount < totalRecipients) {
recipientCount++;
socket.write(`RCPT TO:<recipient${recipientCount}@example.com>\r\n`);
dataBuffer = '';
} else {
console.log(`All ${totalRecipients} recipients accepted`);
step = 'data';
socket.write('DATA\r\n');
dataBuffer = '';
}
} else if (step === 'data' && dataBuffer.includes('354')) {
const recipients = Array.from({length: totalRecipients}, (_, i) => `recipient${i+1}@example.com`);
const email = [
`From: sender@example.com`,
`To: ${recipients.join(', ')}`,
`Subject: Multiple Recipients Routing Test`,
`Date: ${new Date().toUTCString()}`,
`Message-ID: <multi-recipient-${Date.now()}@example.com>`,
'',
'This email tests routing to multiple recipients.',
`Total recipients: ${totalRecipients}`,
'.',
''
].join('\r\n');
socket.write(email);
dataBuffer = '';
step = 'sent';
} else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
if (!completed) {
completed = true;
console.log('Email with multiple recipients routed successfully');
expect(true).toEqual(true);
socket.write('QUIT\r\n');
socket.end();
done.resolve();
}
}
});
socket.on('error', (err) => {
console.error('Socket error:', err);
done.reject(err);
});
await done.promise;
});
tap.test('Email Routing - Invalid domain handling', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
let dataBuffer = '';
let step = 'greeting';
let testType = 'invalid-tld';
const testCases = [
{ email: 'user@invalid-tld', type: 'invalid-tld' },
{ email: 'user@.com', type: 'missing-domain' },
{ email: 'user@domain..com', type: 'double-dot' },
{ email: 'user@-domain.com', type: 'leading-dash' },
{ email: 'user@domain-.com', type: 'trailing-dash' }
];
let currentTest = 0;
socket.on('data', (data) => {
dataBuffer += data.toString();
console.log('Server response:', data.toString());
if (step === 'greeting' && dataBuffer.includes('220 ')) {
step = 'ehlo';
socket.write('EHLO localhost\r\n');
dataBuffer = '';
} else if (step === 'ehlo' && dataBuffer.includes('250')) {
step = 'mail';
socket.write('MAIL FROM:<sender@example.com>\r\n');
dataBuffer = '';
} else if (step === 'mail' && dataBuffer.includes('250')) {
step = 'rcpt';
testType = testCases[currentTest].type;
socket.write(`RCPT TO:<${testCases[currentTest].email}>\r\n`);
dataBuffer = '';
} else if (step === 'rcpt') {
const rejected = dataBuffer.includes('550') || dataBuffer.includes('553') || dataBuffer.includes('501');
console.log(`Invalid domain test (${testType}): ${rejected ? 'properly rejected' : 'unexpectedly accepted'}`);
currentTest++;
if (currentTest < testCases.length) {
// Reset for next test
socket.write('RSET\r\n');
step = 'rset';
dataBuffer = '';
} else {
socket.write('QUIT\r\n');
socket.end();
done.resolve();
}
} else if (step === 'rset' && dataBuffer.includes('250')) {
step = 'mail';
socket.write('MAIL FROM:<sender@example.com>\r\n');
dataBuffer = '';
}
});
socket.on('error', (err) => {
console.error('Socket error:', err);
done.reject(err);
});
await done.promise;
});
tap.test('Email Routing - Mixed local and external recipients', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
let dataBuffer = '';
let step = 'greeting';
let completed = false;
const recipients = [
'local@localhost',
'external@example.com',
'another@localhost',
'remote@external.com'
];
let currentRecipient = 0;
let acceptedRecipients: string[] = [];
socket.on('data', (data) => {
if (completed) return;
dataBuffer += data.toString();
console.log('Server response:', data.toString());
if (step === 'greeting' && dataBuffer.includes('220 ')) {
step = 'ehlo';
socket.write('EHLO localhost\r\n');
dataBuffer = '';
} else if (step === 'ehlo' && dataBuffer.includes('250')) {
step = 'mail';
socket.write('MAIL FROM:<sender@example.com>\r\n');
dataBuffer = '';
} else if (step === 'mail' && dataBuffer.includes('250')) {
step = 'rcpt';
socket.write(`RCPT TO:<${recipients[currentRecipient]}>\r\n`);
dataBuffer = '';
} else if (step === 'rcpt') {
if (dataBuffer.includes('250')) {
acceptedRecipients.push(recipients[currentRecipient]);
console.log(`Recipient ${recipients[currentRecipient]} accepted`);
} else {
console.log(`Recipient ${recipients[currentRecipient]} rejected`);
}
currentRecipient++;
if (currentRecipient < recipients.length) {
socket.write(`RCPT TO:<${recipients[currentRecipient]}>\r\n`);
dataBuffer = '';
} else if (acceptedRecipients.length > 0) {
step = 'data';
socket.write('DATA\r\n');
dataBuffer = '';
} else {
socket.write('QUIT\r\n');
socket.end();
done.resolve();
}
} else if (step === 'data' && dataBuffer.includes('354')) {
const email = [
`From: sender@example.com`,
`To: ${acceptedRecipients.join(', ')}`,
`Subject: Mixed Recipients Routing Test`,
`Date: ${new Date().toUTCString()}`,
`Message-ID: <mixed-routing-${Date.now()}@example.com>`,
'',
'This email tests routing to mixed local and external recipients.',
`Accepted recipients: ${acceptedRecipients.length}`,
'.',
''
].join('\r\n');
socket.write(email);
dataBuffer = '';
step = 'sent';
} else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
if (!completed) {
completed = true;
console.log('Email with mixed recipients routed successfully');
expect(acceptedRecipients.length).toBeGreaterThan(0);
socket.write('QUIT\r\n');
socket.end();
done.resolve();
}
}
});
socket.on('error', (err) => {
console.error('Socket error:', err);
done.reject(err);
});
await done.promise;
});
tap.test('Email Routing - Subdomain routing', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
let dataBuffer = '';
let step = 'greeting';
let completed = false;
const subdomainTests = [
'user@mail.example.com',
'user@smtp.corp.example.com',
'user@deep.sub.domain.example.com'
];
let currentTest = 0;
socket.on('data', (data) => {
if (completed) return;
dataBuffer += data.toString();
console.log('Server response:', data.toString());
if (step === 'greeting' && dataBuffer.includes('220 ')) {
step = 'ehlo';
socket.write('EHLO localhost\r\n');
dataBuffer = '';
} else if (step === 'ehlo' && dataBuffer.includes('250')) {
step = 'mail';
socket.write('MAIL FROM:<sender@example.com>\r\n');
dataBuffer = '';
} else if (step === 'mail' && dataBuffer.includes('250')) {
step = 'rcpt';
socket.write(`RCPT TO:<${subdomainTests[currentTest]}>\r\n`);
dataBuffer = '';
} else if (step === 'rcpt') {
const accepted = dataBuffer.includes('250');
console.log(`Subdomain routing test (${subdomainTests[currentTest]}): ${accepted ? 'accepted' : 'rejected'}`);
currentTest++;
if (currentTest < subdomainTests.length) {
socket.write('RSET\r\n');
step = 'rset';
dataBuffer = '';
} else {
step = 'data';
socket.write('DATA\r\n');
dataBuffer = '';
}
} else if (step === 'rset' && dataBuffer.includes('250')) {
step = 'mail';
socket.write('MAIL FROM:<sender@example.com>\r\n');
dataBuffer = '';
} else if (step === 'data' && dataBuffer.includes('354')) {
const email = [
`From: sender@example.com`,
`To: ${subdomainTests[subdomainTests.length - 1]}`,
`Subject: Subdomain Routing Test`,
`Date: ${new Date().toUTCString()}`,
`Message-ID: <subdomain-routing-${Date.now()}@example.com>`,
'',
'This email tests subdomain routing.',
'.',
''
].join('\r\n');
socket.write(email);
dataBuffer = '';
step = 'sent';
} else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
if (!completed) {
completed = true;
console.log('Subdomain routing test completed');
expect(true).toEqual(true);
socket.write('QUIT\r\n');
socket.end();
done.resolve();
}
}
});
socket.on('error', (err) => {
console.error('Socket error:', err);
done.reject(err);
});
await done.promise;
});
tap.test('cleanup - stop test server', async () => {
await stopTestServer(testServer);
});
tap.start();

View File

@ -0,0 +1,486 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as net from 'net';
import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'
import type { ITestServer } from '../../helpers/server.loader.js';
const TEST_PORT = 2525;
let testServer: ITestServer;
tap.test('setup - start test server', async () => {
testServer = await startTestServer({ port: TEST_PORT });
await new Promise(resolve => setTimeout(resolve, 1000));
});
tap.test('DSN - Extension advertised in EHLO', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
let dataBuffer = '';
socket.on('data', (data) => {
dataBuffer += data.toString();
console.log('Server response:', data.toString());
if (dataBuffer.includes('220 ') && !dataBuffer.includes('EHLO')) {
socket.write('EHLO testclient\r\n');
dataBuffer = '';
} else if (dataBuffer.includes('250')) {
// Check if DSN extension is advertised
const dsnSupported = dataBuffer.toLowerCase().includes('dsn');
console.log('DSN extension advertised:', dsnSupported);
// Parse extensions
const lines = dataBuffer.split('\r\n');
const extensions = lines
.filter(line => line.startsWith('250-') || (line.startsWith('250 ') && lines.indexOf(line) > 0))
.map(line => line.substring(4).split(' ')[0].toUpperCase());
console.log('Server extensions:', extensions);
socket.write('QUIT\r\n');
socket.end();
done.resolve();
}
});
socket.on('error', (err) => {
console.error('Socket error:', err);
done.reject(err);
});
await done.promise;
});
tap.test('DSN - Success notification request', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
let dataBuffer = '';
let step = 'greeting';
let completed = false;
socket.on('data', (data) => {
dataBuffer += data.toString();
console.log('Server response:', data.toString());
if (step === 'greeting' && dataBuffer.includes('220 ')) {
step = 'ehlo';
socket.write('EHLO testclient\r\n');
dataBuffer = '';
} else if (step === 'ehlo' && dataBuffer.includes('250')) {
step = 'mail';
// MAIL FROM with DSN parameters
const envId = `dsn-success-${Date.now()}`;
socket.write(`MAIL FROM:<sender@example.com> RET=FULL ENVID=${envId}\r\n`);
dataBuffer = '';
} else if (step === 'mail') {
const accepted = dataBuffer.includes('250');
const notSupported = dataBuffer.includes('501') || dataBuffer.includes('555');
console.log(`MAIL FROM with DSN: ${accepted ? 'accepted' : notSupported ? 'not supported' : 'error'}`);
if (accepted || notSupported) {
step = 'rcpt';
// Plain MAIL FROM if DSN not supported
if (notSupported) {
socket.write('MAIL FROM:<sender@example.com>\r\n');
dataBuffer = '';
} else {
// RCPT TO with NOTIFY parameter
socket.write('RCPT TO:<recipient@example.com> NOTIFY=SUCCESS\r\n');
dataBuffer = '';
}
}
} else if (step === 'rcpt') {
const accepted = dataBuffer.includes('250');
const notSupported = dataBuffer.includes('501') || dataBuffer.includes('555');
if (notSupported) {
// DSN not supported, try plain RCPT TO
socket.write('RCPT TO:<recipient@example.com>\r\n');
step = 'rcpt_plain';
dataBuffer = '';
} else if (accepted) {
step = 'data';
socket.write('DATA\r\n');
dataBuffer = '';
}
} else if (step === 'rcpt_plain' && dataBuffer.includes('250')) {
step = 'data';
socket.write('DATA\r\n');
dataBuffer = '';
} else if (step === 'data' && dataBuffer.includes('354')) {
const email = [
`From: sender@example.com`,
`To: recipient@example.com`,
`Subject: DSN Test - Success Notification`,
`Date: ${new Date().toUTCString()}`,
`Message-ID: <dsn-success-${Date.now()}@example.com>`,
'',
'This email tests DSN success notification.',
'The server should send a success DSN if supported.',
'.',
''
].join('\r\n');
socket.write(email);
step = 'sent';
dataBuffer = '';
} else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
if (!completed) {
completed = true;
console.log('Email with DSN success request accepted');
expect(true).toEqual(true);
socket.write('QUIT\r\n');
socket.end();
done.resolve();
}
}
});
socket.on('error', (err) => {
console.error('Socket error:', err);
done.reject(err);
});
await done.promise;
});
tap.test('DSN - Multiple notification types', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
let dataBuffer = '';
let step = 'greeting';
let completed = false;
socket.on('data', (data) => {
dataBuffer += data.toString();
console.log('Server response:', data.toString());
if (step === 'greeting' && dataBuffer.includes('220 ')) {
step = 'ehlo';
socket.write('EHLO testclient\r\n');
dataBuffer = '';
} else if (step === 'ehlo' && dataBuffer.includes('250')) {
step = 'mail';
socket.write('MAIL FROM:<sender@example.com>\r\n');
dataBuffer = '';
} else if (step === 'mail' && dataBuffer.includes('250')) {
step = 'rcpt';
// Request multiple notification types
socket.write('RCPT TO:<recipient@example.com> NOTIFY=SUCCESS,FAILURE,DELAY\r\n');
dataBuffer = '';
} else if (step === 'rcpt') {
const accepted = dataBuffer.includes('250');
const notSupported = dataBuffer.includes('501') || dataBuffer.includes('555');
console.log(`Multiple NOTIFY types: ${accepted ? 'accepted' : notSupported ? 'not supported' : 'error'}`);
if (notSupported) {
// Try plain RCPT TO
socket.write('RCPT TO:<recipient@example.com>\r\n');
step = 'rcpt_plain';
dataBuffer = '';
} else if (accepted) {
step = 'data';
socket.write('DATA\r\n');
dataBuffer = '';
}
} else if (step === 'rcpt_plain' && dataBuffer.includes('250')) {
step = 'data';
socket.write('DATA\r\n');
dataBuffer = '';
} else if (step === 'data' && dataBuffer.includes('354')) {
const email = [
`From: sender@example.com`,
`To: recipient@example.com`,
`Subject: DSN Test - Multiple Notifications`,
`Date: ${new Date().toUTCString()}`,
`Message-ID: <dsn-multi-${Date.now()}@example.com>`,
'',
'Testing multiple DSN notification types.',
'.',
''
].join('\r\n');
socket.write(email);
step = 'sent';
dataBuffer = '';
} else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
if (!completed) {
completed = true;
console.log('Email with multiple DSN types accepted');
expect(true).toEqual(true);
socket.write('QUIT\r\n');
socket.end();
done.resolve();
}
}
});
socket.on('error', (err) => {
console.error('Socket error:', err);
done.reject(err);
});
await done.promise;
});
tap.test('DSN - Never notify', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
let dataBuffer = '';
let step = 'greeting';
let completed = false;
socket.on('data', (data) => {
dataBuffer += data.toString();
console.log('Server response:', data.toString());
if (step === 'greeting' && dataBuffer.includes('220 ')) {
step = 'ehlo';
socket.write('EHLO testclient\r\n');
dataBuffer = '';
} else if (step === 'ehlo' && dataBuffer.includes('250')) {
step = 'mail';
socket.write('MAIL FROM:<sender@example.com>\r\n');
dataBuffer = '';
} else if (step === 'mail' && dataBuffer.includes('250')) {
step = 'rcpt';
// Request no notifications
socket.write('RCPT TO:<recipient@example.com> NOTIFY=NEVER\r\n');
dataBuffer = '';
} else if (step === 'rcpt') {
const accepted = dataBuffer.includes('250');
const notSupported = dataBuffer.includes('501') || dataBuffer.includes('555');
console.log(`NOTIFY=NEVER: ${accepted ? 'accepted' : notSupported ? 'not supported' : 'error'}`);
expect(accepted || notSupported).toEqual(true);
if (notSupported) {
socket.write('RCPT TO:<recipient@example.com>\r\n');
step = 'rcpt_plain';
dataBuffer = '';
} else if (accepted) {
step = 'data';
socket.write('DATA\r\n');
dataBuffer = '';
}
} else if (step === 'rcpt_plain' && dataBuffer.includes('250')) {
step = 'data';
socket.write('DATA\r\n');
dataBuffer = '';
} else if (step === 'data' && dataBuffer.includes('354')) {
const email = [
`From: sender@example.com`,
`To: recipient@example.com`,
`Subject: DSN Test - Never Notify`,
`Date: ${new Date().toUTCString()}`,
`Message-ID: <dsn-never-${Date.now()}@example.com>`,
'',
'This email should not generate any DSN.',
'.',
''
].join('\r\n');
socket.write(email);
step = 'sent';
dataBuffer = '';
} else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
if (!completed) {
completed = true;
console.log('Email with NOTIFY=NEVER accepted');
expect(true).toEqual(true);
socket.write('QUIT\r\n');
socket.end();
done.resolve();
}
}
});
socket.on('error', (err) => {
console.error('Socket error:', err);
done.reject(err);
});
await done.promise;
});
tap.test('DSN - Original recipient tracking', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
let dataBuffer = '';
let step = 'greeting';
let completed = false;
socket.on('data', (data) => {
dataBuffer += data.toString();
console.log('Server response:', data.toString());
if (step === 'greeting' && dataBuffer.includes('220 ')) {
step = 'ehlo';
socket.write('EHLO testclient\r\n');
dataBuffer = '';
} else if (step === 'ehlo' && dataBuffer.includes('250')) {
step = 'mail';
socket.write('MAIL FROM:<sender@example.com>\r\n');
dataBuffer = '';
} else if (step === 'mail' && dataBuffer.includes('250')) {
step = 'rcpt';
// Include original recipient for tracking
socket.write('RCPT TO:<recipient@example.com> NOTIFY=FAILURE ORCPT=rfc822;original@example.com\r\n');
dataBuffer = '';
} else if (step === 'rcpt') {
const accepted = dataBuffer.includes('250');
const notSupported = dataBuffer.includes('501') || dataBuffer.includes('555');
console.log(`ORCPT parameter: ${accepted ? 'accepted' : notSupported ? 'not supported' : 'error'}`);
if (notSupported) {
socket.write('RCPT TO:<recipient@example.com>\r\n');
step = 'rcpt_plain';
dataBuffer = '';
} else if (accepted) {
step = 'data';
socket.write('DATA\r\n');
dataBuffer = '';
}
} else if (step === 'rcpt_plain' && dataBuffer.includes('250')) {
step = 'data';
socket.write('DATA\r\n');
dataBuffer = '';
} else if (step === 'data' && dataBuffer.includes('354')) {
const email = [
`From: sender@example.com`,
`To: recipient@example.com`,
`Subject: DSN Test - Original Recipient`,
`Date: ${new Date().toUTCString()}`,
`Message-ID: <dsn-orcpt-${Date.now()}@example.com>`,
'',
'This email tests ORCPT parameter for tracking.',
'.',
''
].join('\r\n');
socket.write(email);
step = 'sent';
dataBuffer = '';
} else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
if (!completed) {
completed = true;
console.log('Email with ORCPT tracking accepted');
expect(true).toEqual(true);
socket.write('QUIT\r\n');
socket.end();
done.resolve();
}
}
});
socket.on('error', (err) => {
console.error('Socket error:', err);
done.reject(err);
});
await done.promise;
});
tap.test('DSN - Return parameter handling', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
let dataBuffer = '';
let step = 'greeting';
socket.on('data', (data) => {
dataBuffer += data.toString();
console.log('Server response:', data.toString());
if (step === 'greeting' && dataBuffer.includes('220 ')) {
step = 'ehlo';
socket.write('EHLO testclient\r\n');
dataBuffer = '';
} else if (step === 'ehlo' && dataBuffer.includes('250')) {
step = 'mail_hdrs';
// Test RET=HDRS
socket.write('MAIL FROM:<sender@example.com> RET=HDRS\r\n');
dataBuffer = '';
} else if (step === 'mail_hdrs') {
const accepted = dataBuffer.includes('250');
const notSupported = dataBuffer.includes('501') || dataBuffer.includes('555');
console.log(`RET=HDRS: ${accepted ? 'accepted' : notSupported ? 'not supported' : 'error'}`);
if (accepted || notSupported) {
// Reset and test RET=FULL
socket.write('RSET\r\n');
step = 'reset';
dataBuffer = '';
}
} else if (step === 'reset' && dataBuffer.includes('250')) {
step = 'mail_full';
socket.write('MAIL FROM:<sender@example.com> RET=FULL\r\n');
dataBuffer = '';
} else if (step === 'mail_full') {
const accepted = dataBuffer.includes('250');
const notSupported = dataBuffer.includes('501') || dataBuffer.includes('555');
console.log(`RET=FULL: ${accepted ? 'accepted' : notSupported ? 'not supported' : 'error'}`);
expect(accepted || notSupported).toEqual(true);
socket.write('QUIT\r\n');
socket.end();
done.resolve();
}
});
socket.on('error', (err) => {
console.error('Socket error:', err);
done.reject(err);
});
await done.promise;
});
tap.test('cleanup - stop test server', async () => {
await stopTestServer(testServer);
});
tap.start();