update
This commit is contained in:
@ -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();
|
Reference in New Issue
Block a user