2025-05-24 16:19:19 +00:00
|
|
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
2025-05-25 11:18:12 +00:00
|
|
|
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';
|
2025-05-24 16:19:19 +00:00
|
|
|
|
2025-05-25 11:18:12 +00:00
|
|
|
let testServer: ITestServer;
|
2025-05-24 16:19:19 +00:00
|
|
|
|
|
|
|
tap.test('setup test SMTP server', async () => {
|
2025-05-25 11:18:12 +00:00
|
|
|
testServer = await startTestServer({
|
|
|
|
port: 2549,
|
|
|
|
tlsEnabled: false,
|
|
|
|
authRequired: false
|
|
|
|
});
|
2025-05-24 16:19:19 +00:00
|
|
|
expect(testServer).toBeTruthy();
|
|
|
|
expect(testServer.port).toBeGreaterThan(0);
|
|
|
|
});
|
|
|
|
|
|
|
|
tap.test('CCMD-09: Basic NOOP command', async () => {
|
|
|
|
const smtpClient = createSmtpClient({
|
|
|
|
host: testServer.hostname,
|
|
|
|
port: testServer.port,
|
|
|
|
secure: false,
|
|
|
|
connectionTimeout: 5000,
|
|
|
|
debug: true
|
|
|
|
});
|
|
|
|
|
|
|
|
await smtpClient.connect();
|
|
|
|
await smtpClient.sendCommand('EHLO testclient.example.com');
|
|
|
|
|
|
|
|
// Send NOOP command
|
|
|
|
const noopResponse = await smtpClient.sendCommand('NOOP');
|
|
|
|
|
|
|
|
// Verify response
|
|
|
|
expect(noopResponse).toInclude('250');
|
|
|
|
console.log(`NOOP response: ${noopResponse.trim()}`);
|
|
|
|
|
|
|
|
await smtpClient.close();
|
|
|
|
});
|
|
|
|
|
|
|
|
tap.test('CCMD-09: NOOP during transaction', async () => {
|
|
|
|
const smtpClient = createSmtpClient({
|
|
|
|
host: testServer.hostname,
|
|
|
|
port: testServer.port,
|
|
|
|
secure: false,
|
|
|
|
connectionTimeout: 5000,
|
|
|
|
debug: true
|
|
|
|
});
|
|
|
|
|
|
|
|
await smtpClient.connect();
|
|
|
|
await smtpClient.sendCommand('EHLO testclient.example.com');
|
|
|
|
|
|
|
|
// Start a transaction
|
|
|
|
await smtpClient.sendCommand('MAIL FROM:<sender@example.com>');
|
|
|
|
await smtpClient.sendCommand('RCPT TO:<recipient@example.com>');
|
|
|
|
|
|
|
|
// Send NOOP - should not affect transaction
|
|
|
|
const noopResponse = await smtpClient.sendCommand('NOOP');
|
|
|
|
expect(noopResponse).toInclude('250');
|
|
|
|
|
|
|
|
// Continue transaction - should still work
|
|
|
|
const dataResponse = await smtpClient.sendCommand('DATA');
|
|
|
|
expect(dataResponse).toInclude('354');
|
|
|
|
|
|
|
|
// Send message
|
|
|
|
const messageResponse = await smtpClient.sendCommand('Subject: Test\r\n\r\nTest message\r\n.');
|
|
|
|
expect(messageResponse).toInclude('250');
|
|
|
|
|
|
|
|
console.log('Transaction completed successfully after NOOP');
|
|
|
|
|
|
|
|
await smtpClient.close();
|
|
|
|
});
|
|
|
|
|
|
|
|
tap.test('CCMD-09: Multiple NOOP commands', async () => {
|
|
|
|
const smtpClient = createSmtpClient({
|
|
|
|
host: testServer.hostname,
|
|
|
|
port: testServer.port,
|
|
|
|
secure: false,
|
|
|
|
connectionTimeout: 5000,
|
|
|
|
debug: true
|
|
|
|
});
|
|
|
|
|
|
|
|
await smtpClient.connect();
|
|
|
|
await smtpClient.sendCommand('EHLO testclient.example.com');
|
|
|
|
|
|
|
|
// Send multiple NOOPs rapidly
|
|
|
|
const noopCount = 10;
|
|
|
|
const responses: string[] = [];
|
|
|
|
|
|
|
|
console.log(`Sending ${noopCount} NOOP commands...`);
|
|
|
|
|
|
|
|
for (let i = 0; i < noopCount; i++) {
|
|
|
|
const response = await smtpClient.sendCommand('NOOP');
|
|
|
|
responses.push(response);
|
|
|
|
}
|
|
|
|
|
|
|
|
// All should succeed
|
|
|
|
responses.forEach((response, index) => {
|
|
|
|
expect(response).toInclude('250');
|
|
|
|
});
|
|
|
|
|
|
|
|
console.log(`All ${noopCount} NOOP commands succeeded`);
|
|
|
|
|
|
|
|
await smtpClient.close();
|
|
|
|
});
|
|
|
|
|
|
|
|
tap.test('CCMD-09: NOOP for keep-alive', async () => {
|
|
|
|
const smtpClient = createSmtpClient({
|
|
|
|
host: testServer.hostname,
|
|
|
|
port: testServer.port,
|
|
|
|
secure: false,
|
|
|
|
connectionTimeout: 10000,
|
|
|
|
debug: true
|
|
|
|
});
|
|
|
|
|
|
|
|
await smtpClient.connect();
|
|
|
|
await smtpClient.sendCommand('EHLO testclient.example.com');
|
|
|
|
|
|
|
|
console.log('Using NOOP for keep-alive over 10 seconds...');
|
|
|
|
|
|
|
|
// Send NOOP every 2 seconds for 10 seconds
|
|
|
|
const keepAliveInterval = 2000;
|
|
|
|
const duration = 10000;
|
|
|
|
const iterations = duration / keepAliveInterval;
|
|
|
|
|
|
|
|
for (let i = 0; i < iterations; i++) {
|
|
|
|
await new Promise(resolve => setTimeout(resolve, keepAliveInterval));
|
|
|
|
|
|
|
|
const startTime = Date.now();
|
|
|
|
const response = await smtpClient.sendCommand('NOOP');
|
|
|
|
const elapsed = Date.now() - startTime;
|
|
|
|
|
|
|
|
expect(response).toInclude('250');
|
|
|
|
console.log(`Keep-alive NOOP ${i + 1}: ${elapsed}ms`);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Connection should still be active
|
|
|
|
expect(smtpClient.isConnected()).toBeTruthy();
|
|
|
|
|
|
|
|
await smtpClient.close();
|
|
|
|
});
|
|
|
|
|
|
|
|
tap.test('CCMD-09: NOOP with parameters', async () => {
|
|
|
|
const smtpClient = createSmtpClient({
|
|
|
|
host: testServer.hostname,
|
|
|
|
port: testServer.port,
|
|
|
|
secure: false,
|
|
|
|
connectionTimeout: 5000,
|
|
|
|
debug: true
|
|
|
|
});
|
|
|
|
|
|
|
|
await smtpClient.connect();
|
|
|
|
await smtpClient.sendCommand('EHLO testclient.example.com');
|
|
|
|
|
|
|
|
// RFC 5321 allows NOOP to have parameters (which are ignored)
|
|
|
|
const noopVariants = [
|
|
|
|
'NOOP',
|
|
|
|
'NOOP test',
|
|
|
|
'NOOP hello world',
|
|
|
|
'NOOP 12345',
|
|
|
|
'NOOP check connection'
|
|
|
|
];
|
|
|
|
|
|
|
|
for (const command of noopVariants) {
|
|
|
|
const response = await smtpClient.sendCommand(command);
|
|
|
|
expect(response).toInclude('250');
|
|
|
|
console.log(`"${command}" -> ${response.trim()}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
await smtpClient.close();
|
|
|
|
});
|
|
|
|
|
|
|
|
tap.test('CCMD-09: NOOP timing analysis', async () => {
|
|
|
|
const smtpClient = createSmtpClient({
|
|
|
|
host: testServer.hostname,
|
|
|
|
port: testServer.port,
|
|
|
|
secure: false,
|
|
|
|
connectionTimeout: 5000,
|
|
|
|
debug: false // Quiet for timing
|
|
|
|
});
|
|
|
|
|
|
|
|
await smtpClient.connect();
|
|
|
|
await smtpClient.sendCommand('EHLO testclient.example.com');
|
|
|
|
|
|
|
|
// Measure NOOP response times
|
|
|
|
const measurements = 20;
|
|
|
|
const times: number[] = [];
|
|
|
|
|
|
|
|
for (let i = 0; i < measurements; i++) {
|
|
|
|
const startTime = Date.now();
|
|
|
|
await smtpClient.sendCommand('NOOP');
|
|
|
|
const elapsed = Date.now() - startTime;
|
|
|
|
times.push(elapsed);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Calculate statistics
|
|
|
|
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
|
|
|
|
const minTime = Math.min(...times);
|
|
|
|
const maxTime = Math.max(...times);
|
|
|
|
|
|
|
|
// Calculate standard deviation
|
|
|
|
const variance = times.reduce((sum, time) => sum + Math.pow(time - avgTime, 2), 0) / times.length;
|
|
|
|
const stdDev = Math.sqrt(variance);
|
|
|
|
|
|
|
|
console.log(`NOOP timing analysis (${measurements} samples):`);
|
|
|
|
console.log(` Average: ${avgTime.toFixed(2)}ms`);
|
|
|
|
console.log(` Min: ${minTime}ms`);
|
|
|
|
console.log(` Max: ${maxTime}ms`);
|
|
|
|
console.log(` Std Dev: ${stdDev.toFixed(2)}ms`);
|
|
|
|
|
|
|
|
// NOOP should be very fast
|
|
|
|
expect(avgTime).toBeLessThan(50);
|
|
|
|
|
|
|
|
// Check for consistency (low standard deviation)
|
|
|
|
expect(stdDev).toBeLessThan(avgTime * 0.5); // Less than 50% of average
|
|
|
|
|
|
|
|
await smtpClient.close();
|
|
|
|
});
|
|
|
|
|
|
|
|
tap.test('CCMD-09: NOOP during DATA phase', async () => {
|
|
|
|
const smtpClient = createSmtpClient({
|
|
|
|
host: testServer.hostname,
|
|
|
|
port: testServer.port,
|
|
|
|
secure: false,
|
|
|
|
connectionTimeout: 5000,
|
|
|
|
debug: true
|
|
|
|
});
|
|
|
|
|
|
|
|
await smtpClient.connect();
|
|
|
|
await smtpClient.sendCommand('EHLO testclient.example.com');
|
|
|
|
|
|
|
|
// Setup transaction
|
|
|
|
await smtpClient.sendCommand('MAIL FROM:<sender@example.com>');
|
|
|
|
await smtpClient.sendCommand('RCPT TO:<recipient@example.com>');
|
|
|
|
|
|
|
|
// Enter DATA phase
|
|
|
|
const dataResponse = await smtpClient.sendCommand('DATA');
|
|
|
|
expect(dataResponse).toInclude('354');
|
|
|
|
|
|
|
|
// During DATA phase, NOOP will be treated as message content
|
|
|
|
await smtpClient.sendCommand('Subject: Test with NOOP');
|
|
|
|
await smtpClient.sendCommand('');
|
|
|
|
await smtpClient.sendCommand('This message contains the word NOOP');
|
|
|
|
await smtpClient.sendCommand('NOOP'); // This is message content, not a command
|
|
|
|
await smtpClient.sendCommand('End of message');
|
|
|
|
|
|
|
|
// End DATA phase
|
|
|
|
const endResponse = await smtpClient.sendCommand('.');
|
|
|
|
expect(endResponse).toInclude('250');
|
|
|
|
|
|
|
|
// Now NOOP should work as a command again
|
|
|
|
const noopResponse = await smtpClient.sendCommand('NOOP');
|
|
|
|
expect(noopResponse).toInclude('250');
|
|
|
|
|
|
|
|
console.log('NOOP works correctly after DATA phase');
|
|
|
|
|
|
|
|
await smtpClient.close();
|
|
|
|
});
|
|
|
|
|
|
|
|
tap.test('CCMD-09: NOOP in pipelined commands', async () => {
|
|
|
|
const smtpClient = createSmtpClient({
|
|
|
|
host: testServer.hostname,
|
|
|
|
port: testServer.port,
|
|
|
|
secure: false,
|
|
|
|
enablePipelining: true,
|
|
|
|
connectionTimeout: 5000,
|
|
|
|
debug: true
|
|
|
|
});
|
|
|
|
|
|
|
|
await smtpClient.connect();
|
|
|
|
await smtpClient.sendCommand('EHLO testclient.example.com');
|
|
|
|
|
|
|
|
// Pipeline NOOP with other commands
|
|
|
|
console.log('Pipelining NOOP with other commands...');
|
|
|
|
|
|
|
|
const pipelinedCommands = [
|
|
|
|
smtpClient.sendCommand('NOOP'),
|
|
|
|
smtpClient.sendCommand('MAIL FROM:<sender@example.com>'),
|
|
|
|
smtpClient.sendCommand('NOOP'),
|
|
|
|
smtpClient.sendCommand('RCPT TO:<recipient@example.com>'),
|
|
|
|
smtpClient.sendCommand('NOOP'),
|
|
|
|
smtpClient.sendCommand('RSET'),
|
|
|
|
smtpClient.sendCommand('NOOP')
|
|
|
|
];
|
|
|
|
|
|
|
|
const responses = await Promise.all(pipelinedCommands);
|
|
|
|
|
|
|
|
// All commands should succeed
|
|
|
|
responses.forEach((response, index) => {
|
|
|
|
expect(response).toInclude('250');
|
|
|
|
});
|
|
|
|
|
|
|
|
console.log('All pipelined commands including NOOPs succeeded');
|
|
|
|
|
|
|
|
await smtpClient.close();
|
|
|
|
});
|
|
|
|
|
|
|
|
tap.test('CCMD-09: NOOP error scenarios', async () => {
|
|
|
|
const smtpClient = createSmtpClient({
|
|
|
|
host: testServer.hostname,
|
|
|
|
port: testServer.port,
|
|
|
|
secure: false,
|
|
|
|
connectionTimeout: 5000,
|
|
|
|
debug: true
|
|
|
|
});
|
|
|
|
|
|
|
|
await smtpClient.connect();
|
|
|
|
|
|
|
|
// Try NOOP before EHLO/HELO (some servers might reject)
|
|
|
|
const earlyNoop = await smtpClient.sendCommand('NOOP');
|
|
|
|
console.log(`NOOP before EHLO: ${earlyNoop.trim()}`);
|
|
|
|
|
|
|
|
// Most servers allow it, but check response
|
|
|
|
expect(earlyNoop).toMatch(/[25]\d\d/);
|
|
|
|
|
|
|
|
// Now do proper handshake
|
|
|
|
await smtpClient.sendCommand('EHLO testclient.example.com');
|
|
|
|
|
|
|
|
// Test malformed NOOP (though it should be accepted)
|
|
|
|
const malformedTests = [
|
|
|
|
'NOOP\t\ttabs',
|
|
|
|
'NOOP multiple spaces',
|
|
|
|
'noop lowercase',
|
|
|
|
'NoOp MixedCase'
|
|
|
|
];
|
|
|
|
|
|
|
|
for (const command of malformedTests) {
|
|
|
|
try {
|
|
|
|
const response = await smtpClient.sendCommand(command);
|
|
|
|
console.log(`"${command}" -> ${response.trim()}`);
|
|
|
|
// Most servers are lenient
|
|
|
|
} catch (error) {
|
|
|
|
console.log(`"${command}" -> Error: ${error.message}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
await smtpClient.close();
|
|
|
|
});
|
|
|
|
|
|
|
|
tap.test('cleanup test SMTP server', async () => {
|
|
|
|
if (testServer) {
|
|
|
|
await testServer.stop();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
export default tap.start();
|