This commit is contained in:
2025-05-26 04:09:29 +00:00
parent 84196f9b13
commit 5a45d6cd45
19 changed files with 2691 additions and 4472 deletions

View File

@ -1,393 +1,438 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as plugins from './plugins.js';
import { createTestServer } from '../../helpers/server.loader.js';
import { createSmtpClient } from '../../helpers/smtp.client.js';
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 type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
import { Email } from '../../../ts/mail/core/classes.email.js';
import * as net from 'net';
tap.test('CEDGE-02: should handle malformed commands gracefully', async (tools) => {
const testId = 'CEDGE-02-malformed-commands';
console.log(`\n${testId}: Testing malformed command handling...`);
let testServer: ITestServer;
let scenarioCount = 0;
tap.test('setup test SMTP server', async () => {
testServer = await startTestServer({
port: 2571,
tlsEnabled: false,
authRequired: false
});
expect(testServer).toBeTruthy();
expect(testServer.port).toEqual(2571);
});
// Scenario 1: Commands with extra spaces
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing commands with extra spaces`);
tap.test('CEDGE-02: Commands with extra spaces', async () => {
// Create server that accepts commands with extra spaces
const spaceyServer = net.createServer((socket) => {
socket.write('220 mail.example.com ESMTP\r\n');
let inData = false;
const testServer = await createTestServer({
onConnection: async (socket) => {
console.log(' [Server] Client connected');
socket.write('220 mail.example.com ESMTP\r\n');
socket.on('data', (data) => {
const lines = data.toString().split('\r\n');
lines.forEach(line => {
if (!line && lines[lines.length - 1] === '') return; // Skip empty trailing line
socket.on('data', (data) => {
const command = data.toString().trim();
console.log(` [Server] Received: ${command}`);
// Accept commands with extra spaces
if (command.match(/^EHLO\s+/i)) {
socket.write('250-mail.example.com\r\n');
socket.write('250 STARTTLS\r\n');
} else if (command.match(/^MAIL\s+FROM:/i)) {
// Even with multiple spaces
socket.write('250 OK\r\n');
} else if (command.match(/^RCPT\s+TO:/i)) {
socket.write('250 OK\r\n');
} else if (command === 'DATA') {
socket.write('354 Start mail input\r\n');
} else if (command === '.') {
socket.write('250 OK\r\n');
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
console.log(`Server received: "${line}"`);
if (inData) {
if (line === '.') {
socket.write('250 Message accepted\r\n');
inData = false;
}
});
}
// Otherwise it's email data, ignore
} else if (line.match(/^EHLO\s+/i)) {
socket.write('250-mail.example.com\r\n');
socket.write('250 OK\r\n');
} else if (line.match(/^MAIL\s+FROM:/i)) {
socket.write('250 OK\r\n');
} else if (line.match(/^RCPT\s+TO:/i)) {
socket.write('250 OK\r\n');
} else if (line === 'DATA') {
socket.write('354 Start mail input\r\n');
inData = true;
} else if (line === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
} else if (line) {
socket.write('500 Command not recognized\r\n');
}
});
});
});
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false
});
await new Promise<void>((resolve) => {
spaceyServer.listen(0, '127.0.0.1', () => resolve());
});
// Test sending with commands that might have extra spaces
const email = new plugins.smartmail.Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Test with extra spaces',
text: 'Testing command formatting'
});
const spaceyPort = (spaceyServer.address() as net.AddressInfo).port;
const result = await smtpClient.sendMail(email);
console.log(` Result: ${result.messageId ? 'Success' : 'Failed'}`);
expect(result).toBeDefined();
expect(result.messageId).toBeDefined();
const smtpClient = createSmtpClient({
host: '127.0.0.1',
port: spaceyPort,
secure: false,
connectionTimeout: 5000,
debug: true
});
await testServer.server.close();
})();
const verified = await smtpClient.verify();
expect(verified).toBeTrue();
// Scenario 2: Commands with incorrect case mixing
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing mixed case commands`);
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Test with extra spaces',
text: 'Testing command formatting'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ Server handled commands with extra spaces');
await smtpClient.close();
spaceyServer.close();
});
tap.test('CEDGE-02: Mixed case commands', async () => {
// Create server that accepts mixed case commands
const mixedCaseServer = net.createServer((socket) => {
socket.write('220 mail.example.com ESMTP\r\n');
let inData = false;
const testServer = await createTestServer({
onConnection: async (socket) => {
console.log(' [Server] Client connected');
socket.write('220 mail.example.com ESMTP\r\n');
socket.on('data', (data) => {
const lines = data.toString().split('\r\n');
lines.forEach(line => {
if (!line && lines[lines.length - 1] === '') return;
socket.on('data', (data) => {
const command = data.toString().trim();
console.log(` [Server] Received: ${command}`);
// RFC says commands should be case-insensitive
const upperCommand = command.toUpperCase();
if (upperCommand.startsWith('EHLO')) {
socket.write('250-mail.example.com\r\n');
socket.write('250 8BITMIME\r\n');
} else if (upperCommand.startsWith('MAIL FROM:')) {
socket.write('250 OK\r\n');
} else if (upperCommand.startsWith('RCPT TO:')) {
socket.write('250 OK\r\n');
} else if (upperCommand === 'DATA') {
socket.write('354 Start mail input\r\n');
} else if (command === '.') {
socket.write('250 OK\r\n');
} else if (upperCommand === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
const upperLine = line.toUpperCase();
console.log(`Server received: "${line}"`);
if (inData) {
if (line === '.') {
socket.write('250 Message accepted\r\n');
inData = false;
}
});
}
} else if (upperLine.startsWith('EHLO')) {
socket.write('250-mail.example.com\r\n');
socket.write('250 8BITMIME\r\n');
} else if (upperLine.startsWith('MAIL FROM:')) {
socket.write('250 OK\r\n');
} else if (upperLine.startsWith('RCPT TO:')) {
socket.write('250 OK\r\n');
} else if (upperLine === 'DATA') {
socket.write('354 Start mail input\r\n');
inData = true;
} else if (upperLine === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
});
});
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false
});
await new Promise<void>((resolve) => {
mixedCaseServer.listen(0, '127.0.0.1', () => resolve());
});
const email = new plugins.smartmail.Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Mixed case test',
text: 'Testing case sensitivity'
});
const mixedPort = (mixedCaseServer.address() as net.AddressInfo).port;
const result = await smtpClient.sendMail(email);
console.log(` Result: ${result.messageId ? 'Success' : 'Failed'}`);
expect(result).toBeDefined();
expect(result.messageId).toBeDefined();
const smtpClient = createSmtpClient({
host: '127.0.0.1',
port: mixedPort,
secure: false,
connectionTimeout: 5000,
debug: true
});
await testServer.server.close();
})();
const verified = await smtpClient.verify();
expect(verified).toBeTrue();
console.log('✅ Server accepts mixed case commands');
// Scenario 3: Commands with missing parameters
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing commands with missing parameters`);
await smtpClient.close();
mixedCaseServer.close();
});
tap.test('CEDGE-02: Commands with missing parameters', async () => {
// Create server that handles incomplete commands
const incompleteServer = net.createServer((socket) => {
socket.write('220 mail.example.com ESMTP\r\n');
const testServer = await createTestServer({
onConnection: async (socket) => {
console.log(' [Server] Client connected');
socket.write('220 mail.example.com ESMTP\r\n');
socket.on('data', (data) => {
const lines = data.toString().split('\r\n');
lines.forEach(line => {
if (!line && lines[lines.length - 1] === '') return;
socket.on('data', (data) => {
const command = data.toString().trim();
console.log(` [Server] Received: ${command}`);
if (command.startsWith('EHLO')) {
if (command === 'EHLO' || command === 'EHLO ') {
// Missing hostname
socket.write('501 Syntax error in parameters or arguments\r\n');
} else {
socket.write('250-mail.example.com\r\n');
socket.write('250 OK\r\n');
}
} else if (command === 'MAIL FROM:' || command === 'MAIL') {
// Missing address
socket.write('501 Syntax error in parameters or arguments\r\n');
} else if (command.startsWith('MAIL FROM:')) {
socket.write('250 OK\r\n');
} else if (command === 'RCPT TO:' || command === 'RCPT') {
// Missing address
socket.write('501 Syntax error in parameters or arguments\r\n');
} else if (command.startsWith('RCPT TO:')) {
socket.write('250 OK\r\n');
} else if (command === 'DATA') {
socket.write('354 Start mail input\r\n');
} else if (command === '.') {
socket.write('250 OK\r\n');
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
console.log(`Server received: "${line}"`);
if (line.startsWith('EHLO')) {
socket.write('250 OK\r\n');
} else if (line === 'MAIL FROM:' || line === 'MAIL FROM') {
// Missing email address
socket.write('501 Syntax error in parameters\r\n');
} else if (line === 'RCPT TO:' || line === 'RCPT TO') {
// Missing recipient
socket.write('501 Syntax error in parameters\r\n');
} else if (line === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
} else if (line) {
socket.write('500 Command not recognized\r\n');
}
});
});
});
await new Promise<void>((resolve) => {
incompleteServer.listen(0, '127.0.0.1', () => resolve());
});
const incompletePort = (incompleteServer.address() as net.AddressInfo).port;
const smtpClient = createSmtpClient({
host: '127.0.0.1',
port: incompletePort,
secure: false,
connectionTimeout: 5000,
debug: true
});
// This should succeed as the client sends proper commands
const verified = await smtpClient.verify();
expect(verified).toBeTrue();
console.log('✅ Client sends properly formatted commands');
await smtpClient.close();
incompleteServer.close();
});
tap.test('CEDGE-02: Commands with extra parameters', async () => {
// Create server that handles commands with extra parameters
const extraParamsServer = net.createServer((socket) => {
socket.write('220 mail.example.com ESMTP\r\n');
let inData = false;
socket.on('data', (data) => {
const lines = data.toString().split('\r\n');
lines.forEach(line => {
if (!line && lines[lines.length - 1] === '') return;
console.log(`Server received: "${line}"`);
if (inData) {
if (line === '.') {
socket.write('250 Message accepted\r\n');
inData = false;
}
});
}
} else if (line.startsWith('EHLO')) {
// Accept EHLO with any parameter
socket.write('250-mail.example.com\r\n');
socket.write('250-SIZE 10240000\r\n');
socket.write('250 8BITMIME\r\n');
} else if (line.match(/^MAIL FROM:.*SIZE=/i)) {
// Accept SIZE parameter
socket.write('250 OK\r\n');
} else if (line.startsWith('MAIL FROM:')) {
socket.write('250 OK\r\n');
} else if (line.startsWith('RCPT TO:')) {
socket.write('250 OK\r\n');
} else if (line === 'DATA') {
socket.write('354 Start mail input\r\n');
inData = true;
} else if (line === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
});
});
// Test with a forgiving client that handles syntax errors
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false
});
await new Promise<void>((resolve) => {
extraParamsServer.listen(0, '127.0.0.1', () => resolve());
});
const email = new plugins.smartmail.Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Syntax error recovery test',
text: 'Testing error recovery'
});
const extraPort = (extraParamsServer.address() as net.AddressInfo).port;
const result = await smtpClient.sendMail(email);
console.log(` Result: ${result.messageId ? 'Success' : 'Failed'}`);
expect(result).toBeDefined();
const smtpClient = createSmtpClient({
host: '127.0.0.1',
port: extraPort,
secure: false,
connectionTimeout: 5000,
debug: true
});
await testServer.server.close();
})();
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Test with parameters',
text: 'Testing extra parameters'
});
// Scenario 4: Commands with invalid syntax
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing invalid command syntax`);
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ Server handled commands with extra parameters');
await smtpClient.close();
extraParamsServer.close();
});
tap.test('CEDGE-02: Invalid command sequences', async () => {
// Create server that enforces command sequence
const sequenceServer = net.createServer((socket) => {
socket.write('220 mail.example.com ESMTP\r\n');
let state = 'GREETING';
const testServer = await createTestServer({
onConnection: async (socket) => {
console.log(' [Server] Client connected');
socket.write('220 mail.example.com ESMTP\r\n');
socket.on('data', (data) => {
const lines = data.toString().split('\r\n');
lines.forEach(line => {
if (!line && lines[lines.length - 1] === '') return;
let state = 'connected';
console.log(`Server received: "${line}" in state ${state}`);
socket.on('data', (data) => {
const command = data.toString().trim();
console.log(` [Server] Received: ${command}`);
if (command.startsWith('EHLO')) {
socket.write('250-mail.example.com\r\n');
socket.write('250 OK\r\n');
state = 'ready';
} else if (command.includes('FROM') && !command.startsWith('MAIL FROM:')) {
// Invalid MAIL command format
socket.write('500 Syntax error, command unrecognized\r\n');
} else if (command.startsWith('MAIL FROM:')) {
socket.write('250 OK\r\n');
state = 'mail';
} else if (command.includes('TO') && !command.startsWith('RCPT TO:')) {
// Invalid RCPT command format
socket.write('500 Syntax error, command unrecognized\r\n');
} else if (command.startsWith('RCPT TO:')) {
socket.write('250 OK\r\n');
state = 'rcpt';
} else if (command === 'DATA') {
socket.write('354 Start mail input\r\n');
state = 'data';
} else if (command === '.' && state === 'data') {
socket.write('250 OK\r\n');
state = 'done';
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
} else if (state !== 'data') {
// Unknown command
socket.write('502 Command not implemented\r\n');
}
});
}
});
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false
});
const email = new plugins.smartmail.Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Invalid syntax test',
text: 'Testing invalid command handling'
});
const result = await smtpClient.sendMail(email);
console.log(` Result: ${result.messageId ? 'Success' : 'Failed'}`);
expect(result).toBeDefined();
await testServer.server.close();
})();
// Scenario 5: Commands sent out of order
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing out-of-order commands`);
const testServer = await createTestServer({
onConnection: async (socket) => {
console.log(' [Server] Client connected');
socket.write('220 mail.example.com ESMTP\r\n');
if (state === 'DATA' && line !== '.') {
// In DATA state, ignore everything except the terminating period
return;
}
let state = 'connected';
socket.on('data', (data) => {
const command = data.toString().trim();
console.log(` [Server] Received: ${command} (state: ${state})`);
if (command.startsWith('EHLO')) {
socket.write('250-mail.example.com\r\n');
socket.write('250 OK\r\n');
state = 'ready';
} else if (command.startsWith('RCPT TO:') && state !== 'mail' && state !== 'rcpt') {
// RCPT before MAIL
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
state = 'READY';
socket.write('250 OK\r\n');
} else if (line.startsWith('MAIL FROM:')) {
if (state !== 'READY') {
socket.write('503 Bad sequence of commands\r\n');
} else if (command.startsWith('DATA') && state !== 'rcpt') {
// DATA before RCPT
} else {
state = 'MAIL';
socket.write('250 OK\r\n');
}
} else if (line.startsWith('RCPT TO:')) {
if (state !== 'MAIL' && state !== 'RCPT') {
socket.write('503 Bad sequence of commands\r\n');
} else if (command.startsWith('MAIL FROM:')) {
if (state === 'ready' || state === 'done') {
socket.write('250 OK\r\n');
state = 'mail';
} else {
socket.write('503 Bad sequence of commands\r\n');
}
} else if (command.startsWith('RCPT TO:')) {
} else {
state = 'RCPT';
socket.write('250 OK\r\n');
state = 'rcpt';
} else if (command === 'DATA') {
socket.write('354 Start mail input\r\n');
state = 'data';
} else if (command === '.' && state === 'data') {
socket.write('250 OK\r\n');
state = 'done';
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
}
} else if (line === 'DATA') {
if (state !== 'RCPT') {
socket.write('503 Bad sequence of commands\r\n');
} else {
state = 'DATA';
socket.write('354 Start mail input\r\n');
}
} else if (line === '.' && state === 'DATA') {
state = 'READY';
socket.write('250 Message accepted\r\n');
} else if (line === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
} else if (line === 'RSET') {
state = 'READY';
socket.write('250 OK\r\n');
}
});
});
});
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false
});
await new Promise<void>((resolve) => {
sequenceServer.listen(0, '127.0.0.1', () => resolve());
});
const email = new plugins.smartmail.Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Command sequence test',
text: 'Testing command ordering'
});
const sequencePort = (sequenceServer.address() as net.AddressInfo).port;
const result = await smtpClient.sendMail(email);
console.log(` Result: ${result.messageId ? 'Success' : 'Failed'}`);
expect(result).toBeDefined();
const smtpClient = createSmtpClient({
host: '127.0.0.1',
port: sequencePort,
secure: false,
connectionTimeout: 5000,
debug: true
});
await testServer.server.close();
})();
// Client should handle proper command sequencing
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Test sequence',
text: 'Testing command sequence'
});
// Scenario 6: Commands with invalid characters
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing commands with invalid characters`);
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ Client maintains proper command sequence');
await smtpClient.close();
sequenceServer.close();
});
tap.test('CEDGE-02: Malformed email addresses', async () => {
// Test how client handles various email formats
const emailServer = net.createServer((socket) => {
socket.write('220 mail.example.com ESMTP\r\n');
let inData = false;
const testServer = await createTestServer({
onConnection: async (socket) => {
console.log(' [Server] Client connected');
socket.write('220 mail.example.com ESMTP\r\n');
socket.on('data', (data) => {
const lines = data.toString().split('\r\n');
lines.forEach(line => {
if (!line && lines[lines.length - 1] === '') return;
socket.on('data', (data) => {
const command = data.toString();
const printable = command.replace(/[\r\n]/g, '\\r\\n').replace(/[^\x20-\x7E]/g, '?');
console.log(` [Server] Received: ${printable}`);
// Check for non-ASCII characters
if (/[^\x00-\x7F]/.test(command)) {
socket.write('500 Syntax error, non-ASCII characters not allowed\r\n');
return;
console.log(`Server received: "${line}"`);
if (inData) {
if (line === '.') {
socket.write('250 Message accepted\r\n');
inData = false;
}
const cleanCommand = command.trim();
if (cleanCommand.startsWith('EHLO')) {
socket.write('250-mail.example.com\r\n');
socket.write('250 OK\r\n');
} else if (cleanCommand.startsWith('MAIL FROM:')) {
socket.write('250 OK\r\n');
} else if (cleanCommand.startsWith('RCPT TO:')) {
socket.write('250 OK\r\n');
} else if (cleanCommand === 'DATA') {
socket.write('354 Start mail input\r\n');
} else if (cleanCommand === '.') {
socket.write('250 OK\r\n');
} else if (cleanCommand === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
}
} else if (line.startsWith('EHLO')) {
socket.write('250 OK\r\n');
} else if (line.startsWith('MAIL FROM:')) {
// Accept any sender format
socket.write('250 OK\r\n');
} else if (line.startsWith('RCPT TO:')) {
// Accept any recipient format
socket.write('250 OK\r\n');
} else if (line === 'DATA') {
socket.write('354 Start mail input\r\n');
inData = true;
} else if (line === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
});
});
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false
});
await new Promise<void>((resolve) => {
emailServer.listen(0, '127.0.0.1', () => resolve());
});
const email = new plugins.smartmail.Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Character encoding test',
text: 'Testing character validation'
});
const emailPort = (emailServer.address() as net.AddressInfo).port;
const result = await smtpClient.sendMail(email);
console.log(` Result: ${result.messageId ? 'Success' : 'Failed'}`);
expect(result).toBeDefined();
const smtpClient = createSmtpClient({
host: '127.0.0.1',
port: emailPort,
secure: false,
connectionTimeout: 5000,
debug: true
});
await testServer.server.close();
})();
// Test with properly formatted email
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Test email formats',
text: 'Testing email address handling'
});
console.log(`\n${testId}: All ${scenarioCount} malformed command scenarios tested ✓`);
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ Client properly formats email addresses');
await smtpClient.close();
emailServer.close();
});
tap.test('cleanup test SMTP server', async () => {
if (testServer) {
await stopTestServer(testServer);
}
});
export default tap.start();