513 lines
15 KiB
TypeScript
513 lines
15 KiB
TypeScript
|
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();
|