dcrouter/test/suite/smtpclient_error-handling/test.cerr-06.invalid-recipients.ts

513 lines
15 KiB
TypeScript
Raw Normal View History

2025-05-24 16:19:19 +00:00
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestSmtpServer } from '../../helpers/server.loader.js';
import { createSmtpClient } from '../../helpers/smtp.client.js';
import { Email } from '../../../ts/mail/core/classes.email.js';
let testServer: any;
tap.test('setup test SMTP server', async () => {
testServer = await startTestSmtpServer();
expect(testServer).toBeTruthy();
expect(testServer.port).toBeGreaterThan(0);
});
tap.test('CERR-06: Invalid email address formats', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
validateEmails: true,
debug: true
});
await smtpClient.connect();
// Test various invalid email formats
const invalidEmails = [
{ email: 'notanemail', error: 'Missing @ symbol' },
{ email: '@example.com', error: 'Missing local part' },
{ email: 'user@', error: 'Missing domain' },
{ email: 'user name@example.com', error: 'Space in local part' },
{ email: 'user@domain with spaces.com', error: 'Space in domain' },
{ email: 'user@@example.com', error: 'Double @ symbol' },
{ email: 'user@.com', error: 'Domain starts with dot' },
{ email: 'user@domain.', error: 'Domain ends with dot' },
{ email: 'user..name@example.com', error: 'Consecutive dots' },
{ email: '.user@example.com', error: 'Starts with dot' },
{ email: 'user.@example.com', error: 'Ends with dot' },
{ email: 'user@domain..com', error: 'Consecutive dots in domain' },
{ email: 'user<>@example.com', error: 'Invalid characters' },
{ email: 'user@domain>.com', error: 'Invalid domain characters' }
];
console.log('Testing invalid email formats:');
for (const test of invalidEmails) {
console.log(`\nTesting: ${test.email} (${test.error})`);
const email = new Email({
from: 'sender@example.com',
to: [test.email],
subject: 'Invalid recipient test',
text: 'Testing invalid email handling'
});
try {
await smtpClient.sendMail(email);
console.log(' Unexpected success - email was accepted');
} catch (error) {
console.log(` Expected error: ${error.message}`);
expect(error.message).toMatch(/invalid|syntax|format|address/i);
}
}
await smtpClient.close();
});
tap.test('CERR-06: Non-existent recipients', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Test non-existent recipients
const nonExistentRecipients = [
'doesnotexist@example.com',
'nosuchuser@example.com',
'randomuser12345@example.com',
'deleted-account@example.com'
];
for (const recipient of nonExistentRecipients) {
console.log(`\nTesting non-existent recipient: ${recipient}`);
const email = new Email({
from: 'sender@example.com',
to: [recipient],
subject: 'Non-existent recipient test',
text: 'Testing non-existent recipient handling'
});
// Monitor RCPT TO response
let rcptResponse = '';
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
const response = await originalSendCommand(command);
if (command.startsWith('RCPT TO')) {
rcptResponse = response;
}
return response;
};
try {
await smtpClient.sendMail(email);
console.log(' Email accepted (may bounce later)');
} catch (error) {
console.log(` Rejected: ${error.message}`);
// Common rejection codes
const rejectionCodes = ['550', '551', '553', '554'];
const hasRejectionCode = rejectionCodes.some(code => error.message.includes(code));
if (hasRejectionCode) {
console.log(' Recipient rejected by server');
}
}
}
await smtpClient.close();
});
tap.test('CERR-06: Mixed valid and invalid recipients', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
continueOnRecipientError: true, // Continue even if some recipients fail
debug: true
});
await smtpClient.connect();
const email = new Email({
from: 'sender@example.com',
to: [
'valid1@example.com',
'invalid@format',
'valid2@example.com',
'nonexistent@example.com',
'valid3@example.com'
],
subject: 'Mixed recipients test',
text: 'Testing mixed valid/invalid recipients'
});
// Track recipient results
const recipientResults: { [email: string]: { accepted: boolean; error?: string } } = {};
smtpClient.on('recipient-result', (result) => {
recipientResults[result.email] = {
accepted: result.accepted,
error: result.error
};
});
console.log('\nSending to mixed valid/invalid recipients...');
try {
const result = await smtpClient.sendMail(email);
console.log('\nResults:');
console.log(` Accepted: ${result.accepted?.length || 0}`);
console.log(` Rejected: ${result.rejected?.length || 0}`);
if (result.accepted && result.accepted.length > 0) {
console.log('\nAccepted recipients:');
result.accepted.forEach(email => console.log(`${email}`));
}
if (result.rejected && result.rejected.length > 0) {
console.log('\nRejected recipients:');
result.rejected.forEach(rejection => {
console.log(`${rejection.email}: ${rejection.reason}`);
});
}
} catch (error) {
console.log('Complete failure:', error.message);
}
await smtpClient.close();
});
tap.test('CERR-06: Recipient validation methods', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Test VRFY command (if supported)
console.log('\nTesting recipient validation methods:');
// 1. VRFY command
try {
const vrfyResponse = await smtpClient.sendCommand('VRFY user@example.com');
console.log('VRFY response:', vrfyResponse.trim());
if (vrfyResponse.startsWith('252')) {
console.log(' Server cannot verify but will accept');
} else if (vrfyResponse.startsWith('250') || vrfyResponse.startsWith('251')) {
console.log(' Address verified');
} else if (vrfyResponse.startsWith('550')) {
console.log(' Address not found');
} else if (vrfyResponse.startsWith('502')) {
console.log(' VRFY command not implemented');
}
} catch (error) {
console.log('VRFY error:', error.message);
}
// 2. EXPN command (if supported)
try {
const expnResponse = await smtpClient.sendCommand('EXPN postmaster');
console.log('\nEXPN response:', expnResponse.trim());
} catch (error) {
console.log('EXPN error:', error.message);
}
// 3. Null sender probe (common validation technique)
console.log('\nTesting null sender probe:');
const probeEmail = new Email({
from: '', // Null sender
to: ['test@example.com'],
subject: 'Address verification probe',
text: 'This is an address verification probe'
});
try {
// Just test RCPT TO, don't actually send
await smtpClient.sendCommand('MAIL FROM:<>');
const rcptResponse = await smtpClient.sendCommand('RCPT TO:<test@example.com>');
console.log('Null sender probe result:', rcptResponse.trim());
await smtpClient.sendCommand('RSET');
} catch (error) {
console.log('Null sender probe failed:', error.message);
}
await smtpClient.close();
});
tap.test('CERR-06: International email addresses', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
supportInternational: true,
debug: true
});
await smtpClient.connect();
// Check for SMTPUTF8 support
const ehloResponse = await smtpClient.sendCommand('EHLO testclient.example.com');
const supportsSmtpUtf8 = ehloResponse.includes('SMTPUTF8');
console.log(`\nSMTPUTF8 support: ${supportsSmtpUtf8}`);
// Test international email addresses
const internationalEmails = [
'user@例え.jp',
'користувач@приклад.укр',
'usuario@ejemplo.españ',
'用户@例子.中国',
'user@tëst.com'
];
for (const recipient of internationalEmails) {
console.log(`\nTesting international address: ${recipient}`);
const email = new Email({
from: 'sender@example.com',
to: [recipient],
subject: 'International recipient test',
text: 'Testing international email addresses'
});
try {
await smtpClient.sendMail(email);
console.log(' Accepted (SMTPUTF8 working)');
} catch (error) {
if (!supportsSmtpUtf8) {
console.log(' Expected rejection - no SMTPUTF8 support');
} else {
console.log(` Error: ${error.message}`);
}
}
}
await smtpClient.close();
});
tap.test('CERR-06: Recipient limits', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 10000,
debug: true
});
await smtpClient.connect();
// Test recipient limits
const recipientCounts = [10, 50, 100, 200, 500, 1000];
for (const count of recipientCounts) {
console.log(`\nTesting ${count} recipients...`);
// Generate recipients
const recipients = Array.from({ length: count }, (_, i) => `user${i}@example.com`);
const email = new Email({
from: 'sender@example.com',
to: recipients,
subject: `Testing ${count} recipients`,
text: 'Testing recipient limits'
});
const startTime = Date.now();
let acceptedCount = 0;
let rejectedCount = 0;
// Monitor RCPT TO responses
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
const response = await originalSendCommand(command);
if (command.startsWith('RCPT TO')) {
if (response.startsWith('250')) {
acceptedCount++;
} else if (response.match(/^[45]/)) {
rejectedCount++;
// Check for recipient limit errors
if (response.match(/too many|limit|maximum/i)) {
console.log(` Recipient limit reached at ${acceptedCount} recipients`);
}
}
}
return response;
};
try {
await smtpClient.sendMail(email);
const elapsed = Date.now() - startTime;
console.log(` Success: ${acceptedCount} accepted, ${rejectedCount} rejected`);
console.log(` Time: ${elapsed}ms (${(elapsed/count).toFixed(2)}ms per recipient)`);
} catch (error) {
console.log(` Error after ${acceptedCount} recipients: ${error.message}`);
if (error.message.match(/too many|limit/i)) {
console.log(' Server recipient limit exceeded');
break; // Don't test higher counts
}
}
// Reset for next test
await smtpClient.sendCommand('RSET');
}
await smtpClient.close();
});
tap.test('CERR-06: Recipient error codes', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Common recipient error codes and their meanings
const errorCodes = [
{ code: '550 5.1.1', meaning: 'User unknown', permanent: true },
{ code: '551 5.1.6', meaning: 'User has moved', permanent: true },
{ code: '552 5.2.2', meaning: 'Mailbox full', permanent: true },
{ code: '553 5.1.3', meaning: 'Invalid address syntax', permanent: true },
{ code: '554 5.7.1', meaning: 'Relay access denied', permanent: true },
{ code: '450 4.1.1', meaning: 'Temporary user lookup failure', permanent: false },
{ code: '451 4.1.8', meaning: 'Sender address rejected', permanent: false },
{ code: '452 4.2.2', meaning: 'Mailbox full (temporary)', permanent: false }
];
console.log('\nRecipient error code reference:');
errorCodes.forEach(error => {
console.log(`\n${error.code}: ${error.meaning}`);
console.log(` Type: ${error.permanent ? 'Permanent failure' : 'Temporary failure'}`);
console.log(` Action: ${error.permanent ? 'Bounce immediately' : 'Queue and retry'}`);
});
await smtpClient.close();
});
tap.test('CERR-06: Catch-all and wildcard handling', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Test catch-all and wildcard addresses
const wildcardTests = [
'*@example.com',
'catchall@*',
'*@*.com',
'user+*@example.com',
'sales-*@example.com'
];
console.log('\nTesting wildcard/catch-all addresses:');
for (const recipient of wildcardTests) {
console.log(`\nTesting: ${recipient}`);
const email = new Email({
from: 'sender@example.com',
to: [recipient],
subject: 'Wildcard test',
text: 'Testing wildcard recipient'
});
try {
await smtpClient.sendMail(email);
console.log(' Accepted (server may expand wildcard)');
} catch (error) {
console.log(` Rejected: ${error.message}`);
// Wildcards typically rejected as invalid syntax
expect(error.message).toMatch(/invalid|syntax|format/i);
}
}
await smtpClient.close();
});
tap.test('CERR-06: Recipient validation timing', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
recipientValidationTimeout: 3000,
debug: true
});
await smtpClient.connect();
// Test validation timing for different scenarios
const timingTests = [
{ email: 'quick@example.com', expectedTime: 'fast' },
{ email: 'slow.lookup@remote-domain.com', expectedTime: 'slow' },
{ email: 'timeout@unresponsive-server.com', expectedTime: 'timeout' }
];
console.log('\nTesting recipient validation timing:');
for (const test of timingTests) {
console.log(`\nValidating: ${test.email}`);
const startTime = Date.now();
try {
await smtpClient.sendCommand(`RCPT TO:<${test.email}>`);
const elapsed = Date.now() - startTime;
console.log(` Response time: ${elapsed}ms`);
console.log(` Expected: ${test.expectedTime}`);
if (test.expectedTime === 'timeout' && elapsed >= 3000) {
console.log(' Validation timed out as expected');
}
} catch (error) {
const elapsed = Date.now() - startTime;
console.log(` Error after ${elapsed}ms: ${error.message}`);
}
await smtpClient.sendCommand('RSET');
}
await smtpClient.close();
});
tap.test('cleanup test SMTP server', async () => {
if (testServer) {
await testServer.stop();
}
});
export default tap.start();