update
This commit is contained in:
@ -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();
|
@ -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();
|
@ -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();
|
528
test/suite/smtpserver_email-processing/test.ep-04.large-email.ts
Normal file
528
test/suite/smtpserver_email-processing/test.ep-04.large-email.ts
Normal 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();
|
@ -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();
|
@ -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();
|
@ -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: supercalifragilisticexpialidocious',
|
||||
'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();
|
@ -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();
|
@ -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();
|
Reference in New Issue
Block a user