This commit is contained in:
2025-05-25 19:02:18 +00:00
parent 5b33623c2d
commit 4c9fd22a86
20 changed files with 1551 additions and 1451 deletions

View File

@ -3,8 +3,10 @@ import { startTestServer, stopTestServer, type ITestServer } from '../../helpers
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 { EmailValidator } from '../../../ts/mail/core/classes.emailvalidator.js';
let testServer: ITestServer;
let smtpClient: SmtpClient;
tap.test('setup test SMTP server', async () => {
testServer = await startTestServer({
@ -14,372 +16,441 @@ tap.test('setup test SMTP server', async () => {
});
expect(testServer).toBeTruthy();
expect(testServer.port).toBeGreaterThan(0);
});
tap.test('CCMD-10: VRFY command basic usage', async () => {
const smtpClient = createSmtpClient({
smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
connectionTimeout: 5000
});
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Test VRFY with various addresses
tap.test('CCMD-10: Email address validation', async () => {
// Test email address validation which is what VRFY conceptually does
const validator = new EmailValidator();
const testAddresses = [
'user@example.com',
'postmaster',
'admin@example.com',
'nonexistent@example.com'
{ address: 'user@example.com', expected: true },
{ address: 'postmaster@example.com', expected: true },
{ address: 'admin@example.com', expected: true },
{ address: 'user.name+tag@example.com', expected: true },
{ address: 'test@sub.domain.example.com', expected: true },
{ address: 'invalid@', expected: false },
{ address: '@example.com', expected: false },
{ address: 'not-an-email', expected: false },
{ address: '', expected: false },
{ address: 'user@', expected: false }
];
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`);
}
console.log('Testing email address validation (VRFY equivalent):\n');
for (const test of testAddresses) {
const isValid = validator.isValidFormat(test.address);
expect(isValid).toEqual(test.expected);
console.log(`Address: "${test.address}" - Valid: ${isValid} (expected: ${test.expected})`);
}
await smtpClient.close();
// Test sending to valid addresses
const validEmail = new Email({
from: 'sender@example.com',
to: ['user@example.com'],
subject: 'Address validation test',
text: 'Testing address validation'
});
await smtpClient.sendMail(validEmail);
console.log('\nEmail sent successfully to validated address');
});
tap.test('CCMD-10: EXPN command basic usage', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
tap.test('CCMD-10: Multiple recipient handling (EXPN equivalent)', async () => {
// Test multiple recipients which is conceptually similar to mailing list expansion
console.log('Testing multiple recipient handling (EXPN equivalent):\n');
// Create email with multiple recipients (like a mailing list)
const multiRecipientEmail = new Email({
from: 'sender@example.com',
to: [
'user1@example.com',
'user2@example.com',
'user3@example.com'
],
cc: [
'cc1@example.com',
'cc2@example.com'
],
bcc: [
'bcc1@example.com'
],
subject: 'Multi-recipient test (mailing list)',
text: 'Testing email distribution to multiple recipients'
});
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();
const toAddresses = multiRecipientEmail.getToAddresses();
const ccAddresses = multiRecipientEmail.getCcAddresses();
const bccAddresses = multiRecipientEmail.getBccAddresses();
console.log(`To recipients: ${toAddresses.length}`);
toAddresses.forEach(addr => console.log(` - ${addr}`));
console.log(`\nCC recipients: ${ccAddresses.length}`);
ccAddresses.forEach(addr => console.log(` - ${addr}`));
console.log(`\nBCC recipients: ${bccAddresses.length}`);
bccAddresses.forEach(addr => console.log(` - ${addr}`));
console.log(`\nTotal recipients: ${toAddresses.length + ccAddresses.length + bccAddresses.length}`);
// Send the email
await smtpClient.sendMail(multiRecipientEmail);
console.log('\nEmail sent successfully to all recipients');
});
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
tap.test('CCMD-10: Email addresses with display names', async () => {
// Test email addresses with display names (full names)
console.log('Testing email addresses with display names:\n');
const fullNameTests = [
'John Doe',
'"Smith, John" <john.smith@example.com>',
'Mary Johnson <mary@example.com>',
'Robert "Bob" Williams'
{ from: '"John Doe" <john@example.com>', expectedAddress: 'john@example.com' },
{ from: '"Smith, John" <john.smith@example.com>', expectedAddress: 'john.smith@example.com' },
{ from: 'Mary Johnson <mary@example.com>', expectedAddress: 'mary@example.com' },
{ from: '<bob@example.com>', expectedAddress: 'bob@example.com' }
];
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)`);
}
for (const test of fullNameTests) {
const email = new Email({
from: test.from,
to: ['recipient@example.com'],
subject: 'Display name test',
text: `Testing from: ${test.from}`
});
}
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 fromAddress = email.getFromAddress();
console.log(`Full: "${test.from}"`);
console.log(`Extracted: "${fromAddress}"`);
expect(fromAddress).toEqual(test.expectedAddress);
await smtpClient.sendMail(email);
console.log('Email sent successfully\n');
}
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' }
tap.test('CCMD-10: Email validation security', async () => {
// Test security aspects of email validation
console.log('Testing email validation security considerations:\n');
// Test common system/role addresses that should be handled carefully
const systemAddresses = [
'root@example.com',
'admin@example.com',
'administrator@example.com',
'webmaster@example.com',
'hostmaster@example.com',
'abuse@example.com',
'postmaster@example.com',
'noreply@example.com'
];
const validator = new EmailValidator();
console.log('Checking if addresses are role accounts:');
for (const addr of systemAddresses) {
const validationResult = await validator.validate(addr, { checkRole: true, checkMx: false });
console.log(` ${addr}: ${validationResult.details?.role ? 'Role account' : 'Not a role account'} (format valid: ${validationResult.details?.formatValid})`);
}
// Test that we don't expose information about which addresses exist
console.log('\nTesting information disclosure prevention:');
try {
// Try sending to a non-existent address
const testEmail = new Email({
from: 'sender@example.com',
to: ['definitely-does-not-exist-12345@example.com'],
subject: 'Test',
text: 'Test'
});
await smtpClient.sendMail(testEmail);
console.log('Server accepted email (does not disclose non-existence)');
} catch (error) {
console.log('Server rejected email:', error.message);
}
console.log('\nSecurity best practice: Servers should not disclose address existence');
});
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}`);
tap.test('CCMD-10: Validation during email sending', async () => {
// Test that validation doesn't interfere with email sending
console.log('Testing validation during email transaction:\n');
const validator = new EmailValidator();
// Create a series of emails with validation between them
const emails = [
{
from: 'sender1@example.com',
to: ['recipient1@example.com'],
subject: 'First email',
text: 'Testing validation during transaction'
},
{
from: 'sender2@example.com',
to: ['recipient2@example.com', 'recipient3@example.com'],
subject: 'Second email',
text: 'Multiple recipients'
},
{
from: '"Test User" <sender3@example.com>',
to: ['recipient4@example.com'],
subject: 'Third email',
text: 'Display name test'
}
];
for (let i = 0; i < emails.length; i++) {
const emailData = emails[i];
// Validate addresses before sending
console.log(`Email ${i + 1}:`);
const fromAddr = emailData.from.includes('<') ? emailData.from.match(/<([^>]+)>/)?.[1] || emailData.from : emailData.from;
console.log(` From: ${emailData.from} - Valid: ${validator.isValidFormat(fromAddr)}`);
for (const to of emailData.to) {
console.log(` To: ${to} - Valid: ${validator.isValidFormat(to)}`);
}
// Create and send email
const email = new Email(emailData);
await smtpClient.sendMail(email);
console.log(` Sent successfully\n`);
}
console.log('All emails sent successfully with validation');
});
tap.test('CCMD-10: Special characters in email addresses', async () => {
// Test email addresses with special characters
console.log('Testing email addresses with special characters:\n');
const validator = new EmailValidator();
const specialAddresses = [
{ address: 'user+tag@example.com', shouldBeValid: true, description: 'Plus addressing' },
{ address: 'first.last@example.com', shouldBeValid: true, description: 'Dots in local part' },
{ address: 'user_name@example.com', shouldBeValid: true, description: 'Underscore' },
{ address: 'user-name@example.com', shouldBeValid: true, description: 'Hyphen' },
{ address: '"quoted string"@example.com', shouldBeValid: true, description: 'Quoted string' },
{ address: 'user@sub.domain.example.com', shouldBeValid: true, description: 'Subdomain' },
{ address: 'user@example.co.uk', shouldBeValid: true, description: 'Multi-part TLD' },
{ address: 'user..name@example.com', shouldBeValid: false, description: 'Double dots' },
{ address: '.user@example.com', shouldBeValid: false, description: 'Leading dot' },
{ address: 'user.@example.com', shouldBeValid: false, description: 'Trailing dot' }
];
for (const test of specialAddresses) {
const isValid = validator.isValidFormat(test.address);
console.log(`${test.description}:`);
console.log(` Address: "${test.address}"`);
console.log(` Valid: ${isValid} (expected: ${test.shouldBeValid})`);
if (test.shouldBeValid && isValid) {
// Try sending an email with this address
try {
const email = new Email({
from: 'sender@example.com',
to: [test.address],
subject: 'Special character test',
text: `Testing special characters in: ${test.address}`
});
await smtpClient.sendMail(email);
console.log(` Email sent successfully`);
} catch (error) {
console.log(` Failed to send: ${error.message}`);
}
}
console.log('');
}
});
tap.test('CCMD-10: Large recipient lists', async () => {
// Test handling of large recipient lists (similar to EXPN multi-line)
console.log('Testing large recipient lists:\n');
// Create email with many recipients
const recipientCount = 20;
const toRecipients = [];
const ccRecipients = [];
for (let i = 1; i <= recipientCount; i++) {
if (i <= 10) {
toRecipients.push(`user${i}@example.com`);
} else {
ccRecipients.push(`user${i}@example.com`);
}
}
console.log(`Creating email with ${recipientCount} total recipients:`);
console.log(` To: ${toRecipients.length} recipients`);
console.log(` CC: ${ccRecipients.length} recipients`);
const largeListEmail = new Email({
from: 'sender@example.com',
to: toRecipients,
cc: ccRecipients,
subject: 'Large distribution list test',
text: `This email is being sent to ${recipientCount} recipients total`
});
// Show extracted addresses
const allTo = largeListEmail.getToAddresses();
const allCc = largeListEmail.getCcAddresses();
console.log('\nExtracted addresses:');
console.log(`To (first 3): ${allTo.slice(0, 3).join(', ')}...`);
console.log(`CC (first 3): ${allCc.slice(0, 3).join(', ')}...`);
// Send the email
const startTime = Date.now();
await smtpClient.sendMail(largeListEmail);
const elapsed = Date.now() - startTime;
console.log(`\nEmail sent to all ${recipientCount} recipients in ${elapsed}ms`);
console.log(`Average: ${(elapsed / recipientCount).toFixed(2)}ms per recipient`);
});
await smtpClient.close();
tap.test('CCMD-10: Email validation performance', async () => {
// Test validation performance
console.log('Testing email validation performance:\n');
const validator = new EmailValidator();
const testCount = 1000;
// Generate test addresses
const testAddresses = [];
for (let i = 0; i < testCount; i++) {
testAddresses.push(`user${i}@example${i % 10}.com`);
}
// Time validation
const startTime = Date.now();
let validCount = 0;
for (const address of testAddresses) {
if (validator.isValidFormat(address)) {
validCount++;
}
}
const elapsed = Date.now() - startTime;
const rate = (testCount / elapsed) * 1000;
console.log(`Validated ${testCount} addresses in ${elapsed}ms`);
console.log(`Rate: ${rate.toFixed(0)} validations/second`);
console.log(`Valid addresses: ${validCount}/${testCount}`);
// Test rapid email sending to see if there's rate limiting
console.log('\nTesting rapid email sending:');
const emailCount = 10;
const sendStartTime = Date.now();
let sentCount = 0;
for (let i = 0; i < emailCount; i++) {
try {
const email = new Email({
from: 'sender@example.com',
to: [`recipient${i}@example.com`],
subject: `Rate test ${i + 1}`,
text: 'Testing rate limits'
});
await smtpClient.sendMail(email);
sentCount++;
} catch (error) {
console.log(`Rate limit hit at email ${i + 1}: ${error.message}`);
break;
}
}
const sendElapsed = Date.now() - sendStartTime;
const sendRate = (sentCount / sendElapsed) * 1000;
console.log(`Sent ${sentCount}/${emailCount} emails in ${sendElapsed}ms`);
console.log(`Rate: ${sendRate.toFixed(2)} emails/second`);
});
tap.test('CCMD-10: Email validation error handling', async () => {
// Test error handling for invalid email addresses
console.log('Testing email validation error handling:\n');
const validator = new EmailValidator();
const errorTests = [
{ address: null, description: 'Null address' },
{ address: undefined, description: 'Undefined address' },
{ address: '', description: 'Empty string' },
{ address: ' ', description: 'Whitespace only' },
{ address: '@', description: 'Just @ symbol' },
{ address: 'user@', description: 'Missing domain' },
{ address: '@domain.com', description: 'Missing local part' },
{ address: 'user@@domain.com', description: 'Double @ symbol' },
{ address: 'user@domain@com', description: 'Multiple @ symbols' },
{ address: 'user space@domain.com', description: 'Space in local part' },
{ address: 'user@domain .com', description: 'Space in domain' },
{ address: 'x'.repeat(256) + '@domain.com', description: 'Very long local part' },
{ address: 'user@' + 'x'.repeat(256) + '.com', description: 'Very long domain' }
];
for (const test of errorTests) {
console.log(`${test.description}:`);
console.log(` Input: "${test.address}"`);
// Test validation
let isValid = false;
try {
isValid = validator.isValidFormat(test.address as any);
} catch (error) {
console.log(` Validation threw: ${error.message}`);
}
if (!isValid) {
console.log(` Correctly rejected as invalid`);
} else {
console.log(` WARNING: Accepted as valid!`);
}
// Try to send email with invalid address
if (test.address) {
try {
const email = new Email({
from: 'sender@example.com',
to: [test.address],
subject: 'Error test',
text: 'Testing invalid address'
});
await smtpClient.sendMail(email);
console.log(` WARNING: Email sent with invalid address!`);
} catch (error) {
console.log(` Email correctly rejected: ${error.message}`);
}
}
console.log('');
}
});
tap.test('cleanup test SMTP server', async () => {
if (testServer) {
await testServer.stop();
await stopTestServer(testServer);
}
});