529 lines
15 KiB
TypeScript
529 lines
15 KiB
TypeScript
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
import * as net from 'net';
|
|
|
|
let testServer: ITestServer;
|
|
|
|
tap.test('setup test SMTP server', async () => {
|
|
testServer = await startTestServer({
|
|
port: 2570,
|
|
tlsEnabled: false,
|
|
authRequired: false
|
|
});
|
|
expect(testServer).toBeTruthy();
|
|
expect(testServer.port).toEqual(2570);
|
|
});
|
|
|
|
tap.test('CEDGE-01: Multi-line greeting', async () => {
|
|
// Create custom server with multi-line greeting
|
|
const customServer = net.createServer((socket) => {
|
|
// Send multi-line greeting
|
|
socket.write('220-mail.example.com ESMTP Server\r\n');
|
|
socket.write('220-Welcome to our mail server!\r\n');
|
|
socket.write('220-Please be patient during busy times.\r\n');
|
|
socket.write('220 Ready to serve\r\n');
|
|
|
|
socket.on('data', (data) => {
|
|
const command = data.toString().trim();
|
|
console.log('Received:', command);
|
|
|
|
if (command.startsWith('EHLO') || command.startsWith('HELO')) {
|
|
socket.write('250 OK\r\n');
|
|
} else if (command === 'QUIT') {
|
|
socket.write('221 Bye\r\n');
|
|
socket.end();
|
|
} else {
|
|
socket.write('500 Command not recognized\r\n');
|
|
}
|
|
});
|
|
});
|
|
|
|
await new Promise<void>((resolve) => {
|
|
customServer.listen(0, '127.0.0.1', () => resolve());
|
|
});
|
|
|
|
const customPort = (customServer.address() as net.AddressInfo).port;
|
|
|
|
const smtpClient = createSmtpClient({
|
|
host: '127.0.0.1',
|
|
port: customPort,
|
|
secure: false,
|
|
connectionTimeout: 5000,
|
|
debug: true
|
|
});
|
|
|
|
console.log('Testing multi-line greeting handling...');
|
|
|
|
const connected = await smtpClient.verify();
|
|
expect(connected).toBeTrue();
|
|
|
|
console.log('Successfully handled multi-line greeting');
|
|
|
|
await smtpClient.close();
|
|
customServer.close();
|
|
});
|
|
|
|
tap.test('CEDGE-01: Slow server responses', async () => {
|
|
// Create server with delayed responses
|
|
const slowServer = net.createServer((socket) => {
|
|
socket.write('220 Slow Server Ready\r\n');
|
|
|
|
socket.on('data', (data) => {
|
|
const command = data.toString().trim();
|
|
console.log('Slow server received:', command);
|
|
|
|
// Add artificial delays
|
|
const delay = 1000 + Math.random() * 2000; // 1-3 seconds
|
|
|
|
setTimeout(() => {
|
|
if (command.startsWith('EHLO')) {
|
|
socket.write('250-slow.example.com\r\n');
|
|
setTimeout(() => socket.write('250 OK\r\n'), 500);
|
|
} else if (command === 'QUIT') {
|
|
socket.write('221 Bye... slowly\r\n');
|
|
setTimeout(() => socket.end(), 1000);
|
|
} else {
|
|
socket.write('250 OK... eventually\r\n');
|
|
}
|
|
}, delay);
|
|
});
|
|
});
|
|
|
|
await new Promise<void>((resolve) => {
|
|
slowServer.listen(0, '127.0.0.1', () => resolve());
|
|
});
|
|
|
|
const slowPort = (slowServer.address() as net.AddressInfo).port;
|
|
|
|
const smtpClient = createSmtpClient({
|
|
host: '127.0.0.1',
|
|
port: slowPort,
|
|
secure: false,
|
|
connectionTimeout: 10000,
|
|
debug: true
|
|
});
|
|
|
|
console.log('\nTesting slow server response handling...');
|
|
const startTime = Date.now();
|
|
|
|
const connected = await smtpClient.verify();
|
|
const connectTime = Date.now() - startTime;
|
|
|
|
expect(connected).toBeTrue();
|
|
console.log(`Connected after ${connectTime}ms (slow server)`);
|
|
expect(connectTime).toBeGreaterThan(1000);
|
|
|
|
await smtpClient.close();
|
|
slowServer.close();
|
|
});
|
|
|
|
tap.test('CEDGE-01: Unusual status codes', async () => {
|
|
// Create server that returns unusual status codes
|
|
const unusualServer = net.createServer((socket) => {
|
|
socket.write('220 Unusual Server\r\n');
|
|
|
|
let commandCount = 0;
|
|
|
|
socket.on('data', (data) => {
|
|
const command = data.toString().trim();
|
|
commandCount++;
|
|
|
|
// Return unusual but valid responses
|
|
if (command.startsWith('EHLO')) {
|
|
socket.write('250-unusual.example.com\r\n');
|
|
socket.write('250-PIPELINING\r\n');
|
|
socket.write('250 OK\r\n'); // Use 250 OK as final response
|
|
} else if (command.startsWith('MAIL FROM')) {
|
|
socket.write('250 Sender OK (#2.0.0)\r\n'); // Valid with enhanced code
|
|
} else if (command.startsWith('RCPT TO')) {
|
|
socket.write('250 Recipient OK\r\n'); // Keep it simple
|
|
} else if (command === 'DATA') {
|
|
socket.write('354 Start mail input\r\n');
|
|
} else if (command === '.') {
|
|
socket.write('250 Message accepted for delivery (#2.0.0)\r\n'); // With enhanced code
|
|
} else if (command === 'QUIT') {
|
|
socket.write('221 Bye (#2.0.0 closing connection)\r\n');
|
|
socket.end();
|
|
} else {
|
|
socket.write('250 OK\r\n'); // Default response
|
|
}
|
|
});
|
|
});
|
|
|
|
await new Promise<void>((resolve) => {
|
|
unusualServer.listen(0, '127.0.0.1', () => resolve());
|
|
});
|
|
|
|
const unusualPort = (unusualServer.address() as net.AddressInfo).port;
|
|
|
|
const smtpClient = createSmtpClient({
|
|
host: '127.0.0.1',
|
|
port: unusualPort,
|
|
secure: false,
|
|
connectionTimeout: 5000,
|
|
debug: true
|
|
});
|
|
|
|
console.log('\nTesting unusual status code handling...');
|
|
|
|
const connected = await smtpClient.verify();
|
|
expect(connected).toBeTrue();
|
|
|
|
const email = new Email({
|
|
from: 'sender@example.com',
|
|
to: ['recipient@example.com'],
|
|
subject: 'Unusual Status Test',
|
|
text: 'Testing unusual server responses'
|
|
});
|
|
|
|
// Should handle unusual codes gracefully
|
|
const result = await smtpClient.sendMail(email);
|
|
console.log('Email sent despite unusual status codes');
|
|
|
|
await smtpClient.close();
|
|
unusualServer.close();
|
|
});
|
|
|
|
tap.test('CEDGE-01: Mixed line endings', async () => {
|
|
// Create server with inconsistent line endings
|
|
const mixedServer = net.createServer((socket) => {
|
|
// Mix CRLF, LF, and CR
|
|
socket.write('220 Mixed line endings server\r\n');
|
|
|
|
socket.on('data', (data) => {
|
|
const command = data.toString().trim();
|
|
|
|
if (command.startsWith('EHLO')) {
|
|
// Mix different line endings
|
|
socket.write('250-mixed.example.com\n'); // LF only
|
|
socket.write('250-PIPELINING\r'); // CR only
|
|
socket.write('250-SIZE 10240000\r\n'); // Proper CRLF
|
|
socket.write('250 8BITMIME\n'); // LF only
|
|
} else if (command === 'QUIT') {
|
|
socket.write('221 Bye\r\n');
|
|
socket.end();
|
|
} else {
|
|
socket.write('250 OK\n'); // LF only
|
|
}
|
|
});
|
|
});
|
|
|
|
await new Promise<void>((resolve) => {
|
|
mixedServer.listen(0, '127.0.0.1', () => resolve());
|
|
});
|
|
|
|
const mixedPort = (mixedServer.address() as net.AddressInfo).port;
|
|
|
|
const smtpClient = createSmtpClient({
|
|
host: '127.0.0.1',
|
|
port: mixedPort,
|
|
secure: false,
|
|
connectionTimeout: 5000,
|
|
debug: true
|
|
});
|
|
|
|
console.log('\nTesting mixed line ending handling...');
|
|
|
|
const connected = await smtpClient.verify();
|
|
expect(connected).toBeTrue();
|
|
|
|
console.log('Successfully handled mixed line endings');
|
|
|
|
await smtpClient.close();
|
|
mixedServer.close();
|
|
});
|
|
|
|
tap.test('CEDGE-01: Empty responses', async () => {
|
|
// Create server that sends minimal but valid responses
|
|
const emptyServer = net.createServer((socket) => {
|
|
socket.write('220 Server with minimal responses\r\n');
|
|
|
|
socket.on('data', (data) => {
|
|
const command = data.toString().trim();
|
|
|
|
if (command.startsWith('EHLO')) {
|
|
// Send minimal but valid EHLO response
|
|
socket.write('250 OK\r\n');
|
|
} else if (command === 'QUIT') {
|
|
socket.write('221 Bye\r\n');
|
|
socket.end();
|
|
} else {
|
|
// Default minimal response
|
|
socket.write('250 OK\r\n');
|
|
}
|
|
});
|
|
});
|
|
|
|
await new Promise<void>((resolve) => {
|
|
emptyServer.listen(0, '127.0.0.1', () => resolve());
|
|
});
|
|
|
|
const emptyPort = (emptyServer.address() as net.AddressInfo).port;
|
|
|
|
const smtpClient = createSmtpClient({
|
|
host: '127.0.0.1',
|
|
port: emptyPort,
|
|
secure: false,
|
|
connectionTimeout: 5000,
|
|
debug: true
|
|
});
|
|
|
|
console.log('\nTesting empty response handling...');
|
|
|
|
const connected = await smtpClient.verify();
|
|
expect(connected).toBeTrue();
|
|
|
|
console.log('Connected successfully with minimal server responses');
|
|
|
|
await smtpClient.close();
|
|
emptyServer.close();
|
|
});
|
|
|
|
tap.test('CEDGE-01: Responses with special characters', async () => {
|
|
// Create server with special characters in responses
|
|
const specialServer = net.createServer((socket) => {
|
|
socket.write('220 ✉️ Unicode SMTP Server 🚀\r\n');
|
|
|
|
socket.on('data', (data) => {
|
|
const command = data.toString().trim();
|
|
|
|
if (command.startsWith('EHLO')) {
|
|
socket.write('250-Hello 你好 مرحبا שלום\r\n');
|
|
socket.write('250-Special chars: <>&"\'`\r\n');
|
|
socket.write('250-Tabs\tand\tspaces here\r\n');
|
|
socket.write('250 OK ✓\r\n');
|
|
} else if (command === 'QUIT') {
|
|
socket.write('221 👋 Goodbye!\r\n');
|
|
socket.end();
|
|
} else {
|
|
socket.write('250 OK 👍\r\n');
|
|
}
|
|
});
|
|
});
|
|
|
|
await new Promise<void>((resolve) => {
|
|
specialServer.listen(0, '127.0.0.1', () => resolve());
|
|
});
|
|
|
|
const specialPort = (specialServer.address() as net.AddressInfo).port;
|
|
|
|
const smtpClient = createSmtpClient({
|
|
host: '127.0.0.1',
|
|
port: specialPort,
|
|
secure: false,
|
|
connectionTimeout: 5000,
|
|
debug: true
|
|
});
|
|
|
|
console.log('\nTesting special character handling...');
|
|
|
|
const connected = await smtpClient.verify();
|
|
expect(connected).toBeTrue();
|
|
|
|
console.log('Successfully handled special characters in responses');
|
|
|
|
await smtpClient.close();
|
|
specialServer.close();
|
|
});
|
|
|
|
tap.test('CEDGE-01: Pipelined responses', async () => {
|
|
// Create server that batches pipelined responses
|
|
const pipelineServer = net.createServer((socket) => {
|
|
socket.write('220 Pipeline Test Server\r\n');
|
|
|
|
let inDataMode = false;
|
|
|
|
socket.on('data', (data) => {
|
|
const commands = data.toString().split('\r\n').filter(cmd => cmd.length > 0);
|
|
|
|
commands.forEach(command => {
|
|
console.log('Pipeline server received:', command);
|
|
|
|
if (inDataMode) {
|
|
if (command === '.') {
|
|
// End of DATA
|
|
socket.write('250 Message accepted\r\n');
|
|
inDataMode = false;
|
|
}
|
|
// Otherwise, we're receiving email data - don't respond
|
|
} else if (command.startsWith('EHLO')) {
|
|
socket.write('250-pipeline.example.com\r\n250-PIPELINING\r\n250 OK\r\n');
|
|
} else if (command.startsWith('MAIL FROM')) {
|
|
socket.write('250 Sender OK\r\n');
|
|
} else if (command.startsWith('RCPT TO')) {
|
|
socket.write('250 Recipient OK\r\n');
|
|
} else if (command === 'DATA') {
|
|
socket.write('354 Send data\r\n');
|
|
inDataMode = true;
|
|
} else if (command === 'QUIT') {
|
|
socket.write('221 Bye\r\n');
|
|
socket.end();
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
await new Promise<void>((resolve) => {
|
|
pipelineServer.listen(0, '127.0.0.1', () => resolve());
|
|
});
|
|
|
|
const pipelinePort = (pipelineServer.address() as net.AddressInfo).port;
|
|
|
|
const smtpClient = createSmtpClient({
|
|
host: '127.0.0.1',
|
|
port: pipelinePort,
|
|
secure: false,
|
|
connectionTimeout: 5000,
|
|
debug: true
|
|
});
|
|
|
|
console.log('\nTesting pipelined responses...');
|
|
|
|
const connected = await smtpClient.verify();
|
|
expect(connected).toBeTrue();
|
|
|
|
// Test sending email with pipelined server
|
|
const email = new Email({
|
|
from: 'sender@example.com',
|
|
to: ['recipient@example.com'],
|
|
subject: 'Pipeline Test',
|
|
text: 'Testing pipelined responses'
|
|
});
|
|
|
|
const result = await smtpClient.sendMail(email);
|
|
expect(result.success).toBeTrue();
|
|
console.log('Successfully handled pipelined responses');
|
|
|
|
await smtpClient.close();
|
|
pipelineServer.close();
|
|
});
|
|
|
|
tap.test('CEDGE-01: Extremely long response lines', async () => {
|
|
const smtpClient = createSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false,
|
|
connectionTimeout: 5000,
|
|
debug: true
|
|
});
|
|
|
|
const connected = await smtpClient.verify();
|
|
expect(connected).toBeTrue();
|
|
|
|
// Create very long message
|
|
const longString = 'x'.repeat(1000);
|
|
|
|
const email = new Email({
|
|
from: 'sender@example.com',
|
|
to: ['recipient@example.com'],
|
|
subject: 'Long line test',
|
|
text: 'Testing long lines',
|
|
headers: {
|
|
'X-Long-Header': longString,
|
|
'X-Another-Long': `Start ${longString} End`
|
|
}
|
|
});
|
|
|
|
console.log('\nTesting extremely long response line handling...');
|
|
|
|
// Note: sendCommand is not a public API method
|
|
// We'll monitor line length through the actual email sending
|
|
let maxLineLength = 1000; // Estimate based on header content
|
|
|
|
const result = await smtpClient.sendMail(email);
|
|
|
|
console.log(`Maximum line length sent: ${maxLineLength} characters`);
|
|
console.log(`RFC 5321 limit: 998 characters (excluding CRLF)`);
|
|
|
|
if (maxLineLength > 998) {
|
|
console.log('WARNING: Line length exceeds RFC limit');
|
|
}
|
|
|
|
expect(result).toBeTruthy();
|
|
|
|
await smtpClient.close();
|
|
});
|
|
|
|
tap.test('CEDGE-01: Server closes connection unexpectedly', async () => {
|
|
// Create server that closes connection at various points
|
|
let closeAfterCommands = 3;
|
|
let commandCount = 0;
|
|
|
|
const abruptServer = net.createServer((socket) => {
|
|
socket.write('220 Abrupt Server\r\n');
|
|
|
|
socket.on('data', (data) => {
|
|
const command = data.toString().trim();
|
|
commandCount++;
|
|
|
|
console.log(`Abrupt server: command ${commandCount} - ${command}`);
|
|
|
|
if (commandCount >= closeAfterCommands) {
|
|
console.log('Abrupt server: Closing connection unexpectedly!');
|
|
socket.destroy(); // Abrupt close
|
|
return;
|
|
}
|
|
|
|
// Normal responses until close
|
|
if (command.startsWith('EHLO')) {
|
|
socket.write('250 OK\r\n');
|
|
} else if (command.startsWith('MAIL FROM')) {
|
|
socket.write('250 OK\r\n');
|
|
} else if (command.startsWith('RCPT TO')) {
|
|
socket.write('250 OK\r\n');
|
|
}
|
|
});
|
|
});
|
|
|
|
await new Promise<void>((resolve) => {
|
|
abruptServer.listen(0, '127.0.0.1', () => resolve());
|
|
});
|
|
|
|
const abruptPort = (abruptServer.address() as net.AddressInfo).port;
|
|
|
|
const smtpClient = createSmtpClient({
|
|
host: '127.0.0.1',
|
|
port: abruptPort,
|
|
secure: false,
|
|
connectionTimeout: 5000,
|
|
debug: true
|
|
});
|
|
|
|
console.log('\nTesting abrupt connection close handling...');
|
|
|
|
// The verify should fail or succeed depending on when the server closes
|
|
const connected = await smtpClient.verify();
|
|
|
|
if (connected) {
|
|
// If verify succeeded, try sending email which should fail
|
|
const email = new Email({
|
|
from: 'sender@example.com',
|
|
to: ['recipient@example.com'],
|
|
subject: 'Abrupt close test',
|
|
text: 'Testing abrupt connection close'
|
|
});
|
|
|
|
try {
|
|
await smtpClient.sendMail(email);
|
|
console.log('Email sent before abrupt close');
|
|
} catch (error) {
|
|
console.log('Expected error due to abrupt close:', error.message);
|
|
expect(error.message).toMatch(/closed|reset|abort|end|timeout/i);
|
|
}
|
|
} else {
|
|
// Verify failed due to abrupt close
|
|
console.log('Connection failed as expected due to abrupt server close');
|
|
}
|
|
|
|
abruptServer.close();
|
|
});
|
|
|
|
tap.test('cleanup test SMTP server', async () => {
|
|
if (testServer) {
|
|
await stopTestServer(testServer);
|
|
}
|
|
});
|
|
|
|
export default tap.start(); |