382 lines
11 KiB
TypeScript
382 lines
11 KiB
TypeScript
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||
|
import { startTestSmtpServer } from '../../helpers/server.loader.js';
|
||
|
import { createSmtpClient } from '../../helpers/smtp.client.js';
|
||
|
|
||
|
let testServer: any;
|
||
|
|
||
|
tap.test('setup test SMTP server', async () => {
|
||
|
testServer = await startTestSmtpServer({
|
||
|
features: ['VRFY', 'EXPN'] // Enable VRFY and EXPN support
|
||
|
});
|
||
|
expect(testServer).toBeTruthy();
|
||
|
expect(testServer.port).toBeGreaterThan(0);
|
||
|
});
|
||
|
|
||
|
tap.test('CCMD-10: VRFY command basic usage', 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');
|
||
|
|
||
|
// Test VRFY with various addresses
|
||
|
const testAddresses = [
|
||
|
'user@example.com',
|
||
|
'postmaster',
|
||
|
'admin@example.com',
|
||
|
'nonexistent@example.com'
|
||
|
];
|
||
|
|
||
|
for (const address of testAddresses) {
|
||
|
const response = await smtpClient.sendCommand(`VRFY ${address}`);
|
||
|
console.log(`VRFY ${address}: ${response.trim()}`);
|
||
|
|
||
|
// Response codes:
|
||
|
// 250 - Address valid
|
||
|
// 251 - Address valid but not local
|
||
|
// 252 - Cannot verify but will accept
|
||
|
// 550 - Address not found
|
||
|
// 502 - Command not implemented
|
||
|
// 252 - Cannot VRFY user
|
||
|
|
||
|
expect(response).toMatch(/^[25]\d\d/);
|
||
|
|
||
|
if (response.startsWith('250') || response.startsWith('251')) {
|
||
|
console.log(` -> Address verified: ${address}`);
|
||
|
} else if (response.startsWith('252')) {
|
||
|
console.log(` -> Cannot verify: ${address}`);
|
||
|
} else if (response.startsWith('550')) {
|
||
|
console.log(` -> Address not found: ${address}`);
|
||
|
} else if (response.startsWith('502')) {
|
||
|
console.log(` -> VRFY not implemented`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
await smtpClient.close();
|
||
|
});
|
||
|
|
||
|
tap.test('CCMD-10: EXPN command basic usage', 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');
|
||
|
|
||
|
// Test EXPN with mailing lists
|
||
|
const testLists = [
|
||
|
'all',
|
||
|
'staff',
|
||
|
'users@example.com',
|
||
|
'mailinglist'
|
||
|
];
|
||
|
|
||
|
for (const list of testLists) {
|
||
|
const response = await smtpClient.sendCommand(`EXPN ${list}`);
|
||
|
console.log(`EXPN ${list}: ${response.trim()}`);
|
||
|
|
||
|
// Response codes:
|
||
|
// 250 - Expansion successful (may be multi-line)
|
||
|
// 252 - Cannot expand
|
||
|
// 550 - List not found
|
||
|
// 502 - Command not implemented
|
||
|
|
||
|
expect(response).toMatch(/^[25]\d\d/);
|
||
|
|
||
|
if (response.startsWith('250')) {
|
||
|
// Multi-line response possible
|
||
|
const lines = response.split('\r\n');
|
||
|
console.log(` -> List expanded to ${lines.length - 1} entries`);
|
||
|
} else if (response.startsWith('252')) {
|
||
|
console.log(` -> Cannot expand list: ${list}`);
|
||
|
} else if (response.startsWith('550')) {
|
||
|
console.log(` -> List not found: ${list}`);
|
||
|
} else if (response.startsWith('502')) {
|
||
|
console.log(` -> EXPN not implemented`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
await smtpClient.close();
|
||
|
});
|
||
|
|
||
|
tap.test('CCMD-10: VRFY with full names', 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');
|
||
|
|
||
|
// Test VRFY with full names
|
||
|
const fullNameTests = [
|
||
|
'John Doe',
|
||
|
'"Smith, John" <john.smith@example.com>',
|
||
|
'Mary Johnson <mary@example.com>',
|
||
|
'Robert "Bob" Williams'
|
||
|
];
|
||
|
|
||
|
for (const name of fullNameTests) {
|
||
|
const response = await smtpClient.sendCommand(`VRFY ${name}`);
|
||
|
console.log(`VRFY "${name}": ${response.trim()}`);
|
||
|
|
||
|
// Check if response includes email address
|
||
|
const emailMatch = response.match(/<([^>]+)>/);
|
||
|
if (emailMatch) {
|
||
|
console.log(` -> Resolved to: ${emailMatch[1]}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
await smtpClient.close();
|
||
|
});
|
||
|
|
||
|
tap.test('CCMD-10: VRFY/EXPN security considerations', 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');
|
||
|
|
||
|
// Many servers disable VRFY/EXPN for security
|
||
|
console.log('\nTesting security responses:');
|
||
|
|
||
|
// Check if commands are disabled
|
||
|
const vrfyResponse = await smtpClient.sendCommand('VRFY postmaster');
|
||
|
const expnResponse = await smtpClient.sendCommand('EXPN all');
|
||
|
|
||
|
if (vrfyResponse.startsWith('502') || vrfyResponse.startsWith('252')) {
|
||
|
console.log('VRFY is disabled or restricted (security best practice)');
|
||
|
}
|
||
|
|
||
|
if (expnResponse.startsWith('502') || expnResponse.startsWith('252')) {
|
||
|
console.log('EXPN is disabled or restricted (security best practice)');
|
||
|
}
|
||
|
|
||
|
// Test potential information disclosure
|
||
|
const probeAddresses = [
|
||
|
'root',
|
||
|
'admin',
|
||
|
'administrator',
|
||
|
'webmaster',
|
||
|
'hostmaster',
|
||
|
'abuse'
|
||
|
];
|
||
|
|
||
|
let disclosureCount = 0;
|
||
|
for (const addr of probeAddresses) {
|
||
|
const response = await smtpClient.sendCommand(`VRFY ${addr}`);
|
||
|
if (response.startsWith('250') || response.startsWith('251')) {
|
||
|
disclosureCount++;
|
||
|
console.log(`Information disclosed for: ${addr}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
console.log(`Total addresses disclosed: ${disclosureCount}/${probeAddresses.length}`);
|
||
|
|
||
|
await smtpClient.close();
|
||
|
});
|
||
|
|
||
|
tap.test('CCMD-10: VRFY/EXPN 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 mail transaction
|
||
|
await smtpClient.sendCommand('MAIL FROM:<sender@example.com>');
|
||
|
await smtpClient.sendCommand('RCPT TO:<recipient@example.com>');
|
||
|
|
||
|
// VRFY/EXPN during transaction should not affect it
|
||
|
const vrfyResponse = await smtpClient.sendCommand('VRFY user@example.com');
|
||
|
console.log(`VRFY during transaction: ${vrfyResponse.trim()}`);
|
||
|
|
||
|
const expnResponse = await smtpClient.sendCommand('EXPN mailinglist');
|
||
|
console.log(`EXPN during transaction: ${expnResponse.trim()}`);
|
||
|
|
||
|
// Continue transaction
|
||
|
const dataResponse = await smtpClient.sendCommand('DATA');
|
||
|
expect(dataResponse).toInclude('354');
|
||
|
|
||
|
await smtpClient.sendCommand('Subject: Test\r\n\r\nTest message\r\n.');
|
||
|
|
||
|
console.log('Transaction completed successfully after VRFY/EXPN');
|
||
|
|
||
|
await smtpClient.close();
|
||
|
});
|
||
|
|
||
|
tap.test('CCMD-10: VRFY with special characters', 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');
|
||
|
|
||
|
// Test addresses with special characters
|
||
|
const specialAddresses = [
|
||
|
'user+tag@example.com',
|
||
|
'first.last@example.com',
|
||
|
'user%remote@example.com',
|
||
|
'"quoted string"@example.com',
|
||
|
'user@[192.168.1.1]',
|
||
|
'user@sub.domain.example.com'
|
||
|
];
|
||
|
|
||
|
for (const addr of specialAddresses) {
|
||
|
const response = await smtpClient.sendCommand(`VRFY ${addr}`);
|
||
|
console.log(`VRFY special address "${addr}": ${response.trim()}`);
|
||
|
}
|
||
|
|
||
|
await smtpClient.close();
|
||
|
});
|
||
|
|
||
|
tap.test('CCMD-10: EXPN multi-line response', 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');
|
||
|
|
||
|
// EXPN might return multiple addresses
|
||
|
const response = await smtpClient.sendCommand('EXPN all-users');
|
||
|
|
||
|
if (response.startsWith('250')) {
|
||
|
const lines = response.split('\r\n').filter(line => line.length > 0);
|
||
|
|
||
|
console.log('EXPN multi-line response:');
|
||
|
lines.forEach((line, index) => {
|
||
|
if (line.includes('250-')) {
|
||
|
// Continuation line
|
||
|
const address = line.substring(4);
|
||
|
console.log(` Member ${index + 1}: ${address}`);
|
||
|
} else if (line.includes('250 ')) {
|
||
|
// Final line
|
||
|
const address = line.substring(4);
|
||
|
console.log(` Member ${index + 1}: ${address} (last)`);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
await smtpClient.close();
|
||
|
});
|
||
|
|
||
|
tap.test('CCMD-10: VRFY/EXPN rate limiting', async () => {
|
||
|
const smtpClient = createSmtpClient({
|
||
|
host: testServer.hostname,
|
||
|
port: testServer.port,
|
||
|
secure: false,
|
||
|
connectionTimeout: 5000,
|
||
|
debug: false // Quiet for rate test
|
||
|
});
|
||
|
|
||
|
await smtpClient.connect();
|
||
|
await smtpClient.sendCommand('EHLO testclient.example.com');
|
||
|
|
||
|
// Send many VRFY commands rapidly
|
||
|
const requestCount = 20;
|
||
|
const startTime = Date.now();
|
||
|
let successCount = 0;
|
||
|
let rateLimitHit = false;
|
||
|
|
||
|
console.log(`Sending ${requestCount} VRFY commands rapidly...`);
|
||
|
|
||
|
for (let i = 0; i < requestCount; i++) {
|
||
|
const response = await smtpClient.sendCommand(`VRFY user${i}@example.com`);
|
||
|
|
||
|
if (response.startsWith('421') || response.startsWith('450')) {
|
||
|
rateLimitHit = true;
|
||
|
console.log(`Rate limit hit at request ${i + 1}`);
|
||
|
break;
|
||
|
} else if (response.match(/^[25]\d\d/)) {
|
||
|
successCount++;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const elapsed = Date.now() - startTime;
|
||
|
const rate = (successCount / elapsed) * 1000;
|
||
|
|
||
|
console.log(`Completed ${successCount} requests in ${elapsed}ms`);
|
||
|
console.log(`Rate: ${rate.toFixed(2)} requests/second`);
|
||
|
|
||
|
if (rateLimitHit) {
|
||
|
console.log('Server implements rate limiting (good security practice)');
|
||
|
}
|
||
|
|
||
|
await smtpClient.close();
|
||
|
});
|
||
|
|
||
|
tap.test('CCMD-10: VRFY/EXPN error handling', 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');
|
||
|
|
||
|
// Test error cases
|
||
|
const errorTests = [
|
||
|
{ command: 'VRFY', description: 'VRFY without parameter' },
|
||
|
{ command: 'EXPN', description: 'EXPN without parameter' },
|
||
|
{ command: 'VRFY @', description: 'VRFY with invalid address' },
|
||
|
{ command: 'EXPN ""', description: 'EXPN with empty string' },
|
||
|
{ command: 'VRFY ' + 'x'.repeat(500), description: 'VRFY with very long parameter' }
|
||
|
];
|
||
|
|
||
|
for (const test of errorTests) {
|
||
|
try {
|
||
|
const response = await smtpClient.sendCommand(test.command);
|
||
|
console.log(`${test.description}: ${response.trim()}`);
|
||
|
|
||
|
// Should get error response
|
||
|
expect(response).toMatch(/^[45]\d\d/);
|
||
|
} catch (error) {
|
||
|
console.log(`${test.description}: Caught error - ${error.message}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
await smtpClient.close();
|
||
|
});
|
||
|
|
||
|
tap.test('cleanup test SMTP server', async () => {
|
||
|
if (testServer) {
|
||
|
await testServer.stop();
|
||
|
}
|
||
|
});
|
||
|
|
||
|
export default tap.start();
|