update
This commit is contained in:
231
test/suite/smtpclient_error-handling/test.cerr-01.4xx-errors.ts
Normal file
231
test/suite/smtpclient_error-handling/test.cerr-01.4xx-errors.ts
Normal file
@ -0,0 +1,231 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
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';
|
||||
|
||||
let testServer: ITestServer;
|
||||
let smtpClient: SmtpClient;
|
||||
|
||||
tap.test('setup - start SMTP server for error handling tests', async () => {
|
||||
testServer = await startTestServer({
|
||||
port: 2550,
|
||||
tlsEnabled: false,
|
||||
authRequired: false,
|
||||
maxRecipients: 5 // Low limit to trigger errors
|
||||
});
|
||||
|
||||
expect(testServer.port).toEqual(2550);
|
||||
});
|
||||
|
||||
tap.test('CERR-01: 4xx Errors - should handle invalid recipient (450)', async () => {
|
||||
smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// Create email with invalid recipient format
|
||||
const email = new Email({
|
||||
from: 'test@example.com',
|
||||
to: 'invalid@address@multiple@signs.com', // Invalid format
|
||||
subject: 'Testing 4xx Error',
|
||||
text: 'This should trigger a 4xx error'
|
||||
});
|
||||
|
||||
let errorCaught = false;
|
||||
try {
|
||||
await smtpClient.sendMail(email);
|
||||
} catch (error) {
|
||||
errorCaught = true;
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
console.log('✅ Invalid recipient error caught:', error.message);
|
||||
}
|
||||
|
||||
expect(errorCaught).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('CERR-01: 4xx Errors - should handle mailbox unavailable (450)', async () => {
|
||||
const email = new Email({
|
||||
from: 'test@example.com',
|
||||
to: 'nonexistent@localhost', // Local domain should trigger mailbox check
|
||||
subject: 'Mailbox Unavailable Test',
|
||||
text: 'Testing mailbox unavailable error'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
|
||||
// Depending on server configuration, this might be accepted or rejected
|
||||
if (!result.success) {
|
||||
expect(result.error).toBeInstanceOf(Error);
|
||||
console.log('✅ Mailbox unavailable handled:', result.error?.message);
|
||||
} else {
|
||||
// Some test servers accept all recipients
|
||||
expect(result.acceptedRecipients.length).toBeGreaterThan(0);
|
||||
console.log('ℹ️ Test server accepted recipient (common in test environments)');
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('CERR-01: 4xx Errors - should handle quota exceeded (452)', async () => {
|
||||
// Send multiple emails to trigger quota/limit errors
|
||||
const emails = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
emails.push(new Email({
|
||||
from: 'test@example.com',
|
||||
to: `recipient${i}@example.com`,
|
||||
subject: `Quota Test ${i}`,
|
||||
text: 'Testing quota limits'
|
||||
}));
|
||||
}
|
||||
|
||||
let quotaErrorCount = 0;
|
||||
const results = await Promise.allSettled(
|
||||
emails.map(email => smtpClient.sendMail(email))
|
||||
);
|
||||
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === 'rejected') {
|
||||
quotaErrorCount++;
|
||||
console.log(`Email ${index} rejected:`, result.reason);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`✅ Handled ${quotaErrorCount} quota-related errors`);
|
||||
});
|
||||
|
||||
tap.test('CERR-01: 4xx Errors - should handle too many recipients (452)', async () => {
|
||||
// Create email with many recipients to exceed limit
|
||||
const recipients = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
recipients.push(`recipient${i}@example.com`);
|
||||
}
|
||||
|
||||
const email = new Email({
|
||||
from: 'test@example.com',
|
||||
to: recipients, // Many recipients
|
||||
subject: 'Too Many Recipients Test',
|
||||
text: 'Testing recipient limit'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
|
||||
// Check if some recipients were rejected due to limits
|
||||
if (result.rejectedRecipients.length > 0) {
|
||||
console.log(`✅ Rejected ${result.rejectedRecipients.length} recipients due to limits`);
|
||||
expect(result.rejectedRecipients).toBeArray();
|
||||
} else {
|
||||
// Server might accept all
|
||||
expect(result.acceptedRecipients.length).toEqual(recipients.length);
|
||||
console.log('ℹ️ Server accepted all recipients');
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('CERR-01: 4xx Errors - should handle authentication required (450)', async () => {
|
||||
// Create new server requiring auth
|
||||
const authServer = await startTestServer({
|
||||
port: 2551,
|
||||
authRequired: true // This will reject unauthenticated commands
|
||||
});
|
||||
|
||||
const unauthClient = createSmtpClient({
|
||||
host: authServer.hostname,
|
||||
port: authServer.port,
|
||||
secure: false,
|
||||
// No auth credentials provided
|
||||
connectionTimeout: 5000
|
||||
});
|
||||
|
||||
const email = new Email({
|
||||
from: 'test@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: 'Auth Required Test',
|
||||
text: 'Should fail without auth'
|
||||
});
|
||||
|
||||
let authError = false;
|
||||
try {
|
||||
await unauthClient.sendMail(email);
|
||||
} catch (error) {
|
||||
authError = true;
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
console.log('✅ Authentication required error caught');
|
||||
}
|
||||
|
||||
expect(authError).toBeTrue();
|
||||
|
||||
await unauthClient.close();
|
||||
await stopTestServer(authServer);
|
||||
});
|
||||
|
||||
tap.test('CERR-01: 4xx Errors - should parse enhanced status codes', async () => {
|
||||
// 4xx errors often include enhanced status codes (e.g., 4.7.1)
|
||||
const email = new Email({
|
||||
from: 'test@blocked-domain.com', // Might trigger policy rejection
|
||||
to: 'recipient@example.com',
|
||||
subject: 'Enhanced Status Code Test',
|
||||
text: 'Testing enhanced status codes'
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await smtpClient.sendMail(email);
|
||||
|
||||
if (!result.success && result.error) {
|
||||
console.log('✅ Error details:', {
|
||||
message: result.error.message,
|
||||
response: result.response
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
// Check if error includes status information
|
||||
expect(error.message).toBeTypeofString();
|
||||
console.log('✅ Error with potential enhanced status:', error.message);
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('CERR-01: 4xx Errors - should not retry permanent 4xx errors', async () => {
|
||||
// Track retry attempts
|
||||
let attemptCount = 0;
|
||||
|
||||
const trackingClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// Monitor connection attempts
|
||||
trackingClient.on('connect', () => attemptCount++);
|
||||
|
||||
const email = new Email({
|
||||
from: 'invalid sender format', // Clearly invalid
|
||||
to: 'recipient@example.com',
|
||||
subject: 'Permanent Error Test',
|
||||
text: 'Should not retry'
|
||||
});
|
||||
|
||||
try {
|
||||
await trackingClient.sendMail(email);
|
||||
} catch (error) {
|
||||
console.log('✅ Permanent error not retried');
|
||||
}
|
||||
|
||||
// Should not have retried
|
||||
expect(attemptCount).toBeLessThanOrEqual(1);
|
||||
|
||||
await trackingClient.close();
|
||||
});
|
||||
|
||||
tap.test('cleanup - close SMTP client', async () => {
|
||||
if (smtpClient && smtpClient.isConnected()) {
|
||||
await smtpClient.close();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('cleanup - stop SMTP server', async () => {
|
||||
await stopTestServer(testServer);
|
||||
});
|
||||
|
||||
tap.start();
|
305
test/suite/smtpclient_error-handling/test.cerr-02.5xx-errors.ts
Normal file
305
test/suite/smtpclient_error-handling/test.cerr-02.5xx-errors.ts
Normal file
@ -0,0 +1,305 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
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';
|
||||
|
||||
let testServer: ITestServer;
|
||||
let smtpClient: SmtpClient;
|
||||
|
||||
tap.test('setup - start SMTP server for 5xx error tests', async () => {
|
||||
testServer = await startTestServer({
|
||||
port: 2552,
|
||||
tlsEnabled: false,
|
||||
authRequired: false,
|
||||
maxRecipients: 3 // Low limit to help trigger errors
|
||||
});
|
||||
|
||||
expect(testServer.port).toEqual(2552);
|
||||
});
|
||||
|
||||
tap.test('CERR-02: 5xx Errors - should handle command not recognized (500)', async () => {
|
||||
smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// The client should handle standard commands properly
|
||||
// This tests that the client doesn't send invalid commands
|
||||
const isConnected = await smtpClient.verify();
|
||||
expect(isConnected).toBeTrue();
|
||||
|
||||
console.log('✅ Client sends only valid SMTP commands');
|
||||
});
|
||||
|
||||
tap.test('CERR-02: 5xx Errors - should handle syntax error (501)', async () => {
|
||||
// Test with malformed email that might cause syntax error
|
||||
let syntaxError = false;
|
||||
|
||||
try {
|
||||
// The Email class should catch this before sending
|
||||
const email = new Email({
|
||||
from: '<invalid>from>@example.com', // Malformed
|
||||
to: 'recipient@example.com',
|
||||
subject: 'Syntax Error Test',
|
||||
text: 'This should fail'
|
||||
});
|
||||
|
||||
await smtpClient.sendMail(email);
|
||||
} catch (error: any) {
|
||||
syntaxError = true;
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
console.log('✅ Syntax error caught:', error.message);
|
||||
}
|
||||
|
||||
expect(syntaxError).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('CERR-02: 5xx Errors - should handle command not implemented (502)', async () => {
|
||||
// Most servers implement all required commands
|
||||
// This test verifies client doesn't use optional/deprecated commands
|
||||
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: 'Standard Commands Test',
|
||||
text: 'Using only standard SMTP commands'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
expect(result.success).toBeTrue();
|
||||
|
||||
console.log('✅ Client uses only widely-implemented commands');
|
||||
});
|
||||
|
||||
tap.test('CERR-02: 5xx Errors - should handle bad sequence (503)', async () => {
|
||||
// The client should maintain proper command sequence
|
||||
// This tests internal state management
|
||||
|
||||
// Send multiple emails to ensure sequence is maintained
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: `Sequence Test ${i}`,
|
||||
text: 'Testing command sequence'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
expect(result.success).toBeTrue();
|
||||
}
|
||||
|
||||
console.log('✅ Client maintains proper command sequence');
|
||||
});
|
||||
|
||||
tap.test('CERR-02: 5xx Errors - should handle authentication failed (535)', async () => {
|
||||
// Create server requiring authentication
|
||||
const authServer = await startTestServer({
|
||||
port: 2553,
|
||||
authRequired: true
|
||||
});
|
||||
|
||||
let authFailed = false;
|
||||
|
||||
try {
|
||||
const badAuthClient = createSmtpClient({
|
||||
host: authServer.hostname,
|
||||
port: authServer.port,
|
||||
secure: false,
|
||||
auth: {
|
||||
user: 'wronguser',
|
||||
pass: 'wrongpass'
|
||||
},
|
||||
connectionTimeout: 5000
|
||||
});
|
||||
|
||||
await badAuthClient.verify();
|
||||
} catch (error: any) {
|
||||
authFailed = true;
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
console.log('✅ Authentication failure (535) handled:', error.message);
|
||||
}
|
||||
|
||||
expect(authFailed).toBeTrue();
|
||||
|
||||
await stopTestServer(authServer);
|
||||
});
|
||||
|
||||
tap.test('CERR-02: 5xx Errors - should handle transaction failed (554)', async () => {
|
||||
// Try to send email that might be rejected
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: 'postmaster@[127.0.0.1]', // IP literal might be rejected
|
||||
subject: 'Transaction Test',
|
||||
text: 'Testing transaction failure'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
|
||||
// Depending on server configuration
|
||||
if (!result.success) {
|
||||
console.log('✅ Transaction failure handled gracefully');
|
||||
expect(result.error).toBeInstanceOf(Error);
|
||||
} else {
|
||||
console.log('ℹ️ Test server accepted IP literal recipient');
|
||||
expect(result.acceptedRecipients.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('CERR-02: 5xx Errors - should not retry permanent 5xx errors', async () => {
|
||||
let attemptCount = 0;
|
||||
|
||||
// Create a client that tracks connection attempts
|
||||
const trackingClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000
|
||||
});
|
||||
|
||||
trackingClient.on('connect', () => attemptCount++);
|
||||
|
||||
// Try to send with permanently invalid data
|
||||
try {
|
||||
const email = new Email({
|
||||
from: '', // Empty from
|
||||
to: 'recipient@example.com',
|
||||
subject: 'Permanent Error Test',
|
||||
text: 'Should not retry'
|
||||
});
|
||||
|
||||
await trackingClient.sendMail(email);
|
||||
} catch (error) {
|
||||
console.log('✅ Permanent error not retried');
|
||||
}
|
||||
|
||||
// Should not retry permanent errors
|
||||
expect(attemptCount).toBeLessThanOrEqual(1);
|
||||
|
||||
await trackingClient.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-02: 5xx Errors - should handle server unavailable (550)', async () => {
|
||||
// Test with recipient that might be rejected
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: 'no-such-user@localhost',
|
||||
subject: 'User Unknown Test',
|
||||
text: 'Testing unknown user rejection'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
|
||||
if (result.rejectedRecipients.length > 0) {
|
||||
console.log('✅ Unknown user (550) rejection handled');
|
||||
expect(result.rejectedRecipients).toContain('no-such-user@localhost');
|
||||
} else {
|
||||
// Test server might accept all
|
||||
console.log('ℹ️ Test server accepted unknown user');
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('CERR-02: 5xx Errors - should close connection after fatal error', async () => {
|
||||
// Test that client properly closes connection after fatal errors
|
||||
const fatalClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000
|
||||
});
|
||||
|
||||
// Verify connection works
|
||||
await fatalClient.verify();
|
||||
expect(fatalClient.isConnected()).toBeTrue();
|
||||
|
||||
// Simulate a scenario that might cause fatal error
|
||||
// In real scenarios, this might be server shutdown, etc.
|
||||
|
||||
// For this test, we'll close and verify state
|
||||
await fatalClient.close();
|
||||
expect(fatalClient.isConnected()).toBeFalse();
|
||||
|
||||
console.log('✅ Connection properly closed after errors');
|
||||
});
|
||||
|
||||
tap.test('CERR-02: 5xx Errors - should provide detailed error information', async () => {
|
||||
// Test error detail extraction
|
||||
let errorDetails: any = null;
|
||||
|
||||
try {
|
||||
const email = new Email({
|
||||
from: 'a'.repeat(100) + '@example.com', // Very long local part
|
||||
to: 'recipient@example.com',
|
||||
subject: 'Error Details Test',
|
||||
text: 'Testing error details'
|
||||
});
|
||||
|
||||
await smtpClient.sendMail(email);
|
||||
} catch (error: any) {
|
||||
errorDetails = error;
|
||||
}
|
||||
|
||||
if (errorDetails) {
|
||||
expect(errorDetails).toBeInstanceOf(Error);
|
||||
expect(errorDetails.message).toBeTypeofString();
|
||||
console.log('✅ Detailed error information provided:', errorDetails.message);
|
||||
} else {
|
||||
console.log('ℹ️ Long email address accepted by validator');
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('CERR-02: 5xx Errors - should handle multiple 5xx errors gracefully', async () => {
|
||||
// Send several emails that might trigger different 5xx errors
|
||||
const testEmails = [
|
||||
{
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@invalid-tld', // Invalid TLD
|
||||
subject: 'Invalid TLD Test',
|
||||
text: 'Test 1'
|
||||
},
|
||||
{
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@.com', // Missing domain part
|
||||
subject: 'Missing Domain Test',
|
||||
text: 'Test 2'
|
||||
},
|
||||
{
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: 'Valid Email After Errors',
|
||||
text: 'This should work'
|
||||
}
|
||||
];
|
||||
|
||||
let successCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
for (const emailData of testEmails) {
|
||||
try {
|
||||
const email = new Email(emailData);
|
||||
const result = await smtpClient.sendMail(email);
|
||||
if (result.success) successCount++;
|
||||
} catch (error) {
|
||||
errorCount++;
|
||||
console.log(` Error for ${emailData.to}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ Handled multiple errors: ${errorCount} errors, ${successCount} successes`);
|
||||
expect(successCount).toBeGreaterThan(0); // At least the valid email should work
|
||||
});
|
||||
|
||||
tap.test('cleanup - close SMTP client', async () => {
|
||||
if (smtpClient && smtpClient.isConnected()) {
|
||||
await smtpClient.close();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('cleanup - stop SMTP server', async () => {
|
||||
await stopTestServer(testServer);
|
||||
});
|
||||
|
||||
tap.start();
|
@ -0,0 +1,363 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
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';
|
||||
import * as net from 'net';
|
||||
|
||||
let testServer: ITestServer;
|
||||
|
||||
tap.test('setup - start SMTP server for network failure tests', async () => {
|
||||
testServer = await startTestServer({
|
||||
port: 2554,
|
||||
tlsEnabled: false,
|
||||
authRequired: false
|
||||
});
|
||||
|
||||
expect(testServer.port).toEqual(2554);
|
||||
});
|
||||
|
||||
tap.test('CERR-03: Network Failures - should handle connection refused', async () => {
|
||||
let errorCaught = false;
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Try to connect to a port that's not listening
|
||||
const client = createSmtpClient({
|
||||
host: 'localhost',
|
||||
port: 9876, // Non-listening port
|
||||
secure: false,
|
||||
connectionTimeout: 3000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
await client.verify();
|
||||
} catch (error: any) {
|
||||
errorCaught = true;
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(error.message).toContain('ECONNREFUSED');
|
||||
console.log(`✅ Connection refused handled in ${duration}ms`);
|
||||
}
|
||||
|
||||
expect(errorCaught).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('CERR-03: Network Failures - should handle DNS resolution failure', async () => {
|
||||
let dnsError = false;
|
||||
|
||||
try {
|
||||
const client = createSmtpClient({
|
||||
host: 'non.existent.domain.that.should.not.resolve.example',
|
||||
port: 25,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
await client.verify();
|
||||
} catch (error: any) {
|
||||
dnsError = true;
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
console.log('✅ DNS resolution failure handled:', error.code);
|
||||
}
|
||||
|
||||
expect(dnsError).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('CERR-03: Network Failures - should handle connection drop during handshake', async () => {
|
||||
// Create a server that drops connections immediately
|
||||
const dropServer = net.createServer((socket) => {
|
||||
// Drop connection after accepting
|
||||
socket.destroy();
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
dropServer.listen(2555, () => resolve());
|
||||
});
|
||||
|
||||
let dropError = false;
|
||||
|
||||
try {
|
||||
const client = createSmtpClient({
|
||||
host: 'localhost',
|
||||
port: 2555,
|
||||
secure: false,
|
||||
connectionTimeout: 5000
|
||||
});
|
||||
|
||||
await client.verify();
|
||||
} catch (error: any) {
|
||||
dropError = true;
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
console.log('✅ Connection drop during handshake handled');
|
||||
}
|
||||
|
||||
expect(dropError).toBeTrue();
|
||||
|
||||
dropServer.close();
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
});
|
||||
|
||||
tap.test('CERR-03: Network Failures - should handle connection drop during data transfer', async () => {
|
||||
const client = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
socketTimeout: 10000
|
||||
});
|
||||
|
||||
// Establish connection first
|
||||
await client.verify();
|
||||
|
||||
// For this test, we simulate network issues by attempting
|
||||
// to send after server issues might occur
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: 'Network Failure Test',
|
||||
text: 'Testing network failure recovery'
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await client.sendMail(email);
|
||||
expect(result.success).toBeTrue();
|
||||
console.log('✅ Email sent successfully (no network failure simulated)');
|
||||
} catch (error) {
|
||||
console.log('✅ Network failure handled during data transfer');
|
||||
}
|
||||
|
||||
await client.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-03: Network Failures - should retry on transient network errors', async () => {
|
||||
let attemptCount = 0;
|
||||
|
||||
// Create a server that fails first attempt
|
||||
const retryServer = net.createServer((socket) => {
|
||||
attemptCount++;
|
||||
if (attemptCount === 1) {
|
||||
// First attempt: drop connection
|
||||
socket.destroy();
|
||||
} else {
|
||||
// Second attempt: normal SMTP
|
||||
socket.write('220 Retry server ready\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
if (command.startsWith('EHLO') || command.startsWith('HELO')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
retryServer.listen(2556, () => resolve());
|
||||
});
|
||||
|
||||
const client = createSmtpClient({
|
||||
host: 'localhost',
|
||||
port: 2556,
|
||||
secure: false,
|
||||
connectionTimeout: 5000
|
||||
});
|
||||
|
||||
// Client might or might not retry depending on implementation
|
||||
try {
|
||||
await client.verify();
|
||||
console.log(`✅ Connection established after ${attemptCount} attempts`);
|
||||
} catch (error) {
|
||||
console.log(`✅ Network error handled after ${attemptCount} attempts`);
|
||||
}
|
||||
|
||||
retryServer.close();
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
});
|
||||
|
||||
tap.test('CERR-03: Network Failures - should handle slow network (timeout)', async () => {
|
||||
// Create a server that responds very slowly
|
||||
const slowServer = net.createServer((socket) => {
|
||||
// Wait 5 seconds before responding
|
||||
setTimeout(() => {
|
||||
socket.write('220 Slow server ready\r\n');
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
slowServer.listen(2557, () => resolve());
|
||||
});
|
||||
|
||||
let timeoutError = false;
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const client = createSmtpClient({
|
||||
host: 'localhost',
|
||||
port: 2557,
|
||||
secure: false,
|
||||
connectionTimeout: 2000 // 2 second timeout
|
||||
});
|
||||
|
||||
await client.verify();
|
||||
} catch (error: any) {
|
||||
timeoutError = true;
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(duration).toBeLessThan(3000);
|
||||
console.log(`✅ Slow network timeout after ${duration}ms`);
|
||||
}
|
||||
|
||||
expect(timeoutError).toBeTrue();
|
||||
|
||||
slowServer.close();
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
});
|
||||
|
||||
tap.test('CERR-03: Network Failures - should recover from temporary network issues', async () => {
|
||||
const client = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 2,
|
||||
connectionTimeout: 5000
|
||||
});
|
||||
|
||||
// Send first email successfully
|
||||
const email1 = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: 'Before Network Issue',
|
||||
text: 'First email'
|
||||
});
|
||||
|
||||
const result1 = await client.sendMail(email1);
|
||||
expect(result1.success).toBeTrue();
|
||||
|
||||
// Simulate network recovery by sending another email
|
||||
const email2 = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: 'After Network Recovery',
|
||||
text: 'Second email after recovery'
|
||||
});
|
||||
|
||||
const result2 = await client.sendMail(email2);
|
||||
expect(result2.success).toBeTrue();
|
||||
|
||||
console.log('✅ Recovered from simulated network issues');
|
||||
|
||||
await client.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-03: Network Failures - should handle EHOSTUNREACH', async () => {
|
||||
let unreachableError = false;
|
||||
|
||||
try {
|
||||
// Use an IP that should be unreachable
|
||||
const client = createSmtpClient({
|
||||
host: '192.0.2.1', // TEST-NET-1, should be unreachable
|
||||
port: 25,
|
||||
secure: false,
|
||||
connectionTimeout: 3000
|
||||
});
|
||||
|
||||
await client.verify();
|
||||
} catch (error: any) {
|
||||
unreachableError = true;
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
console.log('✅ Host unreachable error handled:', error.code);
|
||||
}
|
||||
|
||||
expect(unreachableError).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('CERR-03: Network Failures - should handle packet loss simulation', async () => {
|
||||
// Create a server that randomly drops data
|
||||
let packetCount = 0;
|
||||
const lossyServer = net.createServer((socket) => {
|
||||
socket.write('220 Lossy server ready\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
packetCount++;
|
||||
|
||||
// Simulate 30% packet loss
|
||||
if (Math.random() > 0.3) {
|
||||
const command = data.toString().trim();
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
}
|
||||
// Otherwise, don't respond (simulate packet loss)
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
lossyServer.listen(2558, () => resolve());
|
||||
});
|
||||
|
||||
const client = createSmtpClient({
|
||||
host: 'localhost',
|
||||
port: 2558,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
socketTimeout: 2000 // Short timeout to detect loss
|
||||
});
|
||||
|
||||
try {
|
||||
await client.verify();
|
||||
console.log('✅ Connected despite simulated packet loss');
|
||||
} catch (error) {
|
||||
console.log(`✅ Packet loss detected after ${packetCount} packets`);
|
||||
}
|
||||
|
||||
lossyServer.close();
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
});
|
||||
|
||||
tap.test('CERR-03: Network Failures - should provide meaningful error messages', async () => {
|
||||
const errorScenarios = [
|
||||
{
|
||||
host: 'localhost',
|
||||
port: 9999,
|
||||
expectedError: 'ECONNREFUSED'
|
||||
},
|
||||
{
|
||||
host: 'invalid.domain.test',
|
||||
port: 25,
|
||||
expectedError: 'ENOTFOUND'
|
||||
}
|
||||
];
|
||||
|
||||
for (const scenario of errorScenarios) {
|
||||
try {
|
||||
const client = createSmtpClient({
|
||||
host: scenario.host,
|
||||
port: scenario.port,
|
||||
secure: false,
|
||||
connectionTimeout: 3000
|
||||
});
|
||||
|
||||
await client.verify();
|
||||
} catch (error: any) {
|
||||
expect(error.message).toBeTypeofString();
|
||||
console.log(`✅ Clear error for ${scenario.host}:${scenario.port} - ${error.code || error.message}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('cleanup - stop SMTP server', async () => {
|
||||
await stopTestServer(testServer);
|
||||
});
|
||||
|
||||
tap.start();
|
@ -0,0 +1,492 @@
|
||||
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';
|
||||
import * as net from 'net';
|
||||
|
||||
let testServer: any;
|
||||
|
||||
tap.test('setup test SMTP server', async () => {
|
||||
testServer = await startTestSmtpServer();
|
||||
expect(testServer).toBeTruthy();
|
||||
expect(testServer.port).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('CERR-04: Basic greylisting response', async () => {
|
||||
// Create server that simulates greylisting
|
||||
const greylistServer = net.createServer((socket) => {
|
||||
let attemptCount = 0;
|
||||
const greylistDuration = 2000; // 2 seconds for testing
|
||||
const firstAttemptTime = Date.now();
|
||||
|
||||
socket.write('220 Greylist Test Server\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO') || command.startsWith('HELO')) {
|
||||
socket.write('250-greylist.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO')) {
|
||||
attemptCount++;
|
||||
const elapsed = Date.now() - firstAttemptTime;
|
||||
|
||||
if (attemptCount === 1 || elapsed < greylistDuration) {
|
||||
// First attempt or within greylist period - reject
|
||||
socket.write('451 4.7.1 Greylisting in effect, please retry later\r\n');
|
||||
} else {
|
||||
// After greylist period - accept
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Send data\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
greylistServer.listen(0, '127.0.0.1', () => resolve());
|
||||
});
|
||||
|
||||
const greylistPort = (greylistServer.address() as net.AddressInfo).port;
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: greylistPort,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
greylistingRetry: true,
|
||||
greylistingDelay: 2500, // Wait 2.5 seconds before retry
|
||||
debug: true
|
||||
});
|
||||
|
||||
console.log('Testing greylisting handling...');
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Greylisting Test',
|
||||
text: 'Testing greylisting retry logic'
|
||||
});
|
||||
|
||||
const startTime = Date.now();
|
||||
let retryCount = 0;
|
||||
|
||||
smtpClient.on('greylisting', (info) => {
|
||||
retryCount++;
|
||||
console.log(`Greylisting detected, retry ${retryCount}: ${info.message}`);
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await smtpClient.sendMail(email);
|
||||
const elapsed = Date.now() - startTime;
|
||||
|
||||
console.log(`Email sent successfully after ${elapsed}ms`);
|
||||
console.log(`Retries due to greylisting: ${retryCount}`);
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(elapsed).toBeGreaterThan(2000); // Should include retry delay
|
||||
} catch (error) {
|
||||
console.log('Send failed:', error.message);
|
||||
}
|
||||
|
||||
await smtpClient.close();
|
||||
greylistServer.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-04: Different greylisting response codes', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
// Test recognition of various greylisting responses
|
||||
const greylistResponses = [
|
||||
{ code: '451 4.7.1', message: 'Greylisting in effect, please retry', isGreylist: true },
|
||||
{ code: '450 4.7.1', message: 'Try again later', isGreylist: true },
|
||||
{ code: '451 4.7.0', message: 'Temporary rejection', isGreylist: true },
|
||||
{ code: '421 4.7.0', message: 'Too many connections, try later', isGreylist: false },
|
||||
{ code: '452 4.2.2', message: 'Mailbox full', isGreylist: false },
|
||||
{ code: '451', message: 'Requested action aborted', isGreylist: true }
|
||||
];
|
||||
|
||||
console.log('\nTesting greylisting response recognition:');
|
||||
|
||||
for (const response of greylistResponses) {
|
||||
console.log(`\nResponse: ${response.code} ${response.message}`);
|
||||
|
||||
// Check if response matches greylisting patterns
|
||||
const isGreylistPattern =
|
||||
(response.code.startsWith('450') || response.code.startsWith('451')) &&
|
||||
(response.message.toLowerCase().includes('grey') ||
|
||||
response.message.toLowerCase().includes('try') ||
|
||||
response.message.toLowerCase().includes('later') ||
|
||||
response.code.includes('4.7.'));
|
||||
|
||||
console.log(` Detected as greylisting: ${isGreylistPattern}`);
|
||||
console.log(` Expected: ${response.isGreylist}`);
|
||||
|
||||
expect(isGreylistPattern).toEqual(response.isGreylist);
|
||||
}
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-04: Greylisting retry strategies', async () => {
|
||||
// Test different retry strategies
|
||||
const strategies = [
|
||||
{
|
||||
name: 'Fixed delay',
|
||||
delays: [300, 300, 300], // Same delay each time
|
||||
maxRetries: 3
|
||||
},
|
||||
{
|
||||
name: 'Exponential backoff',
|
||||
delays: [300, 600, 1200], // Double each time
|
||||
maxRetries: 3
|
||||
},
|
||||
{
|
||||
name: 'Fibonacci sequence',
|
||||
delays: [300, 300, 600, 900, 1500], // Fibonacci-like
|
||||
maxRetries: 5
|
||||
},
|
||||
{
|
||||
name: 'Random jitter',
|
||||
delays: [250 + Math.random() * 100, 250 + Math.random() * 100, 250 + Math.random() * 100],
|
||||
maxRetries: 3
|
||||
}
|
||||
];
|
||||
|
||||
console.log('\nGreylisting retry strategies:');
|
||||
|
||||
for (const strategy of strategies) {
|
||||
console.log(`\n${strategy.name}:`);
|
||||
console.log(` Max retries: ${strategy.maxRetries}`);
|
||||
console.log(` Delays: ${strategy.delays.map(d => `${d.toFixed(0)}ms`).join(', ')}`);
|
||||
|
||||
let totalTime = 0;
|
||||
strategy.delays.forEach((delay, i) => {
|
||||
totalTime += delay;
|
||||
console.log(` After retry ${i + 1}: ${totalTime.toFixed(0)}ms total`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('CERR-04: Greylisting with multiple recipients', async () => {
|
||||
// Create server that greylists per recipient
|
||||
const perRecipientGreylist = net.createServer((socket) => {
|
||||
const recipientAttempts: { [key: string]: number } = {};
|
||||
const recipientFirstSeen: { [key: string]: number } = {};
|
||||
|
||||
socket.write('220 Per-recipient Greylist Server\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO') || command.startsWith('HELO')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO')) {
|
||||
const recipientMatch = command.match(/<([^>]+)>/);
|
||||
if (recipientMatch) {
|
||||
const recipient = recipientMatch[1];
|
||||
|
||||
if (!recipientAttempts[recipient]) {
|
||||
recipientAttempts[recipient] = 0;
|
||||
recipientFirstSeen[recipient] = Date.now();
|
||||
}
|
||||
|
||||
recipientAttempts[recipient]++;
|
||||
const elapsed = Date.now() - recipientFirstSeen[recipient];
|
||||
|
||||
// Different greylisting duration per domain
|
||||
const greylistDuration = recipient.endsWith('@important.com') ? 3000 : 1000;
|
||||
|
||||
if (recipientAttempts[recipient] === 1 || elapsed < greylistDuration) {
|
||||
socket.write(`451 4.7.1 Recipient ${recipient} is greylisted\r\n`);
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
}
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Send data\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
perRecipientGreylist.listen(0, '127.0.0.1', () => resolve());
|
||||
});
|
||||
|
||||
const greylistPort = (perRecipientGreylist.address() as net.AddressInfo).port;
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: greylistPort,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
console.log('\nTesting per-recipient greylisting...');
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: [
|
||||
'user1@normal.com',
|
||||
'user2@important.com',
|
||||
'user3@normal.com'
|
||||
],
|
||||
subject: 'Multi-recipient Greylisting Test',
|
||||
text: 'Testing greylisting with multiple recipients'
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log('Initial attempt result:', result);
|
||||
} catch (error) {
|
||||
console.log('Expected greylisting error:', error.message);
|
||||
|
||||
// Wait and retry
|
||||
console.log('Waiting before retry...');
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
|
||||
try {
|
||||
const retryResult = await smtpClient.sendMail(email);
|
||||
console.log('Retry result:', retryResult);
|
||||
} catch (retryError) {
|
||||
console.log('Some recipients still greylisted:', retryError.message);
|
||||
}
|
||||
}
|
||||
|
||||
await smtpClient.close();
|
||||
perRecipientGreylist.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-04: Greylisting persistence across connections', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
greylistingCache: true, // Enable greylisting cache
|
||||
debug: true
|
||||
});
|
||||
|
||||
// First attempt
|
||||
console.log('\nFirst connection attempt...');
|
||||
await smtpClient.connect();
|
||||
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Greylisting Cache Test',
|
||||
text: 'Testing greylisting cache'
|
||||
});
|
||||
|
||||
let firstAttemptTime: number | null = null;
|
||||
|
||||
try {
|
||||
await smtpClient.sendMail(email);
|
||||
} catch (error) {
|
||||
if (error.message.includes('451') || error.message.includes('grey')) {
|
||||
firstAttemptTime = Date.now();
|
||||
console.log('First attempt greylisted:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
await smtpClient.close();
|
||||
|
||||
// Simulate delay
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Second attempt with new connection
|
||||
console.log('\nSecond connection attempt...');
|
||||
await smtpClient.connect();
|
||||
|
||||
if (firstAttemptTime && smtpClient.getGreylistCache) {
|
||||
const cacheEntry = smtpClient.getGreylistCache('sender@example.com', 'recipient@example.com');
|
||||
if (cacheEntry) {
|
||||
console.log(`Greylisting cache hit: first seen ${Date.now() - firstAttemptTime}ms ago`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log('Second attempt successful');
|
||||
} catch (error) {
|
||||
console.log('Second attempt failed:', error.message);
|
||||
}
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-04: Greylisting timeout handling', async () => {
|
||||
// Server with very long greylisting period
|
||||
const timeoutGreylistServer = net.createServer((socket) => {
|
||||
socket.write('220 Timeout Test Server\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO')) {
|
||||
// Always greylist
|
||||
socket.write('451 4.7.1 Please try again in 30 minutes\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
timeoutGreylistServer.listen(0, '127.0.0.1', () => resolve());
|
||||
});
|
||||
|
||||
const timeoutPort = (timeoutGreylistServer.address() as net.AddressInfo).port;
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: timeoutPort,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
greylistingRetry: true,
|
||||
greylistingMaxRetries: 3,
|
||||
greylistingDelay: 1000,
|
||||
greylistingMaxWait: 5000, // Max 5 seconds total wait
|
||||
debug: true
|
||||
});
|
||||
|
||||
console.log('\nTesting greylisting timeout...');
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Timeout Test',
|
||||
text: 'Testing greylisting timeout'
|
||||
});
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
await smtpClient.sendMail(email);
|
||||
console.log('Unexpected success');
|
||||
} catch (error) {
|
||||
const elapsed = Date.now() - startTime;
|
||||
console.log(`Failed after ${elapsed}ms: ${error.message}`);
|
||||
|
||||
// Should fail within max wait time
|
||||
expect(elapsed).toBeLessThan(6000);
|
||||
expect(error.message).toMatch(/grey|retry|timeout/i);
|
||||
}
|
||||
|
||||
await smtpClient.close();
|
||||
timeoutGreylistServer.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-04: Greylisting statistics', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
greylistingStats: true,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// Track greylisting events
|
||||
const stats = {
|
||||
totalAttempts: 0,
|
||||
greylistedResponses: 0,
|
||||
successfulAfterGreylist: 0,
|
||||
averageDelay: 0,
|
||||
delays: [] as number[]
|
||||
};
|
||||
|
||||
smtpClient.on('send-attempt', () => {
|
||||
stats.totalAttempts++;
|
||||
});
|
||||
|
||||
smtpClient.on('greylisting', (info) => {
|
||||
stats.greylistedResponses++;
|
||||
if (info.delay) {
|
||||
stats.delays.push(info.delay);
|
||||
}
|
||||
});
|
||||
|
||||
smtpClient.on('send-success', (info) => {
|
||||
if (info.wasGreylisted) {
|
||||
stats.successfulAfterGreylist++;
|
||||
}
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
// Simulate multiple sends with greylisting
|
||||
const emails = Array.from({ length: 5 }, (_, i) => new Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i}@example.com`],
|
||||
subject: `Test ${i}`,
|
||||
text: 'Testing greylisting statistics'
|
||||
}));
|
||||
|
||||
for (const email of emails) {
|
||||
try {
|
||||
await smtpClient.sendMail(email);
|
||||
} catch (error) {
|
||||
// Some might fail
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate statistics
|
||||
if (stats.delays.length > 0) {
|
||||
stats.averageDelay = stats.delays.reduce((a, b) => a + b, 0) / stats.delays.length;
|
||||
}
|
||||
|
||||
console.log('\nGreylisting Statistics:');
|
||||
console.log(` Total attempts: ${stats.totalAttempts}`);
|
||||
console.log(` Greylisted responses: ${stats.greylistedResponses}`);
|
||||
console.log(` Successful after greylist: ${stats.successfulAfterGreylist}`);
|
||||
console.log(` Average delay: ${stats.averageDelay.toFixed(0)}ms`);
|
||||
console.log(` Greylist rate: ${((stats.greylistedResponses / stats.totalAttempts) * 100).toFixed(1)}%`);
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('cleanup test SMTP server', async () => {
|
||||
if (testServer) {
|
||||
await testServer.stop();
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
@ -0,0 +1,583 @@
|
||||
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';
|
||||
import * as net from 'net';
|
||||
|
||||
let testServer: any;
|
||||
|
||||
tap.test('setup test SMTP server', async () => {
|
||||
testServer = await startTestSmtpServer();
|
||||
expect(testServer).toBeTruthy();
|
||||
expect(testServer.port).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('CERR-05: Mailbox quota exceeded', async () => {
|
||||
// Create server that simulates quota exceeded
|
||||
const quotaServer = net.createServer((socket) => {
|
||||
socket.write('220 Quota Test Server\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO') || command.startsWith('HELO')) {
|
||||
socket.write('250-quota.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO')) {
|
||||
const recipient = command.match(/<([^>]+)>/)?.[1] || '';
|
||||
|
||||
// Different quota scenarios
|
||||
if (recipient.includes('full')) {
|
||||
socket.write('452 4.2.2 Mailbox full, try again later\r\n');
|
||||
} else if (recipient.includes('over')) {
|
||||
socket.write('552 5.2.2 Mailbox quota exceeded\r\n');
|
||||
} else if (recipient.includes('system')) {
|
||||
socket.write('452 4.3.1 Insufficient system storage\r\n');
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Send data\r\n');
|
||||
} else if (command === '.') {
|
||||
// Check message size
|
||||
socket.write('552 5.3.4 Message too big for system\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
quotaServer.listen(0, '127.0.0.1', () => resolve());
|
||||
});
|
||||
|
||||
const quotaPort = (quotaServer.address() as net.AddressInfo).port;
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: quotaPort,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
console.log('Testing quota exceeded errors...');
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
// Test different quota scenarios
|
||||
const quotaTests = [
|
||||
{
|
||||
to: 'user@full.example.com',
|
||||
expectedCode: '452',
|
||||
expectedError: 'temporary',
|
||||
description: 'Temporary mailbox full'
|
||||
},
|
||||
{
|
||||
to: 'user@over.example.com',
|
||||
expectedCode: '552',
|
||||
expectedError: 'permanent',
|
||||
description: 'Permanent quota exceeded'
|
||||
},
|
||||
{
|
||||
to: 'user@system.example.com',
|
||||
expectedCode: '452',
|
||||
expectedError: 'temporary',
|
||||
description: 'System storage issue'
|
||||
}
|
||||
];
|
||||
|
||||
for (const test of quotaTests) {
|
||||
console.log(`\nTesting: ${test.description}`);
|
||||
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: [test.to],
|
||||
subject: 'Quota Test',
|
||||
text: 'Testing quota errors'
|
||||
});
|
||||
|
||||
try {
|
||||
await smtpClient.sendMail(email);
|
||||
console.log('Unexpected success');
|
||||
} catch (error) {
|
||||
console.log(`Error: ${error.message}`);
|
||||
expect(error.message).toInclude(test.expectedCode);
|
||||
|
||||
if (test.expectedError === 'temporary') {
|
||||
expect(error.code).toMatch(/^4/);
|
||||
} else {
|
||||
expect(error.code).toMatch(/^5/);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await smtpClient.close();
|
||||
quotaServer.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-05: Message size quota', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
// Check SIZE extension
|
||||
const ehloResponse = await smtpClient.sendCommand('EHLO testclient.example.com');
|
||||
const sizeMatch = ehloResponse.match(/250[- ]SIZE (\d+)/);
|
||||
|
||||
if (sizeMatch) {
|
||||
const maxSize = parseInt(sizeMatch[1]);
|
||||
console.log(`Server advertises max message size: ${maxSize} bytes`);
|
||||
}
|
||||
|
||||
// Create messages of different sizes
|
||||
const messageSizes = [
|
||||
{ size: 1024, description: '1 KB' },
|
||||
{ size: 1024 * 1024, description: '1 MB' },
|
||||
{ size: 10 * 1024 * 1024, description: '10 MB' },
|
||||
{ size: 50 * 1024 * 1024, description: '50 MB' }
|
||||
];
|
||||
|
||||
for (const test of messageSizes) {
|
||||
console.log(`\nTesting message size: ${test.description}`);
|
||||
|
||||
// Create large content
|
||||
const largeContent = 'x'.repeat(test.size);
|
||||
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: `Size test: ${test.description}`,
|
||||
text: largeContent
|
||||
});
|
||||
|
||||
// Monitor SIZE parameter in MAIL FROM
|
||||
let sizeParam = '';
|
||||
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
|
||||
|
||||
smtpClient.sendCommand = async (command: string) => {
|
||||
if (command.startsWith('MAIL FROM') && command.includes('SIZE=')) {
|
||||
const match = command.match(/SIZE=(\d+)/);
|
||||
if (match) {
|
||||
sizeParam = match[1];
|
||||
console.log(` SIZE parameter: ${sizeParam} bytes`);
|
||||
}
|
||||
}
|
||||
return originalSendCommand(command);
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await smtpClient.sendMail(email);
|
||||
console.log(` Result: Success`);
|
||||
} catch (error) {
|
||||
console.log(` Result: ${error.message}`);
|
||||
|
||||
// Check for size-related errors
|
||||
if (error.message.match(/552|5\.2\.3|5\.3\.4|size|big|large/i)) {
|
||||
console.log(' Message rejected due to size');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-05: Disk quota vs mailbox quota', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
// Different quota error types
|
||||
const quotaErrors = [
|
||||
{
|
||||
code: '452 4.2.2',
|
||||
message: 'Mailbox full',
|
||||
type: 'user-quota-soft',
|
||||
retry: true
|
||||
},
|
||||
{
|
||||
code: '552 5.2.2',
|
||||
message: 'Mailbox quota exceeded',
|
||||
type: 'user-quota-hard',
|
||||
retry: false
|
||||
},
|
||||
{
|
||||
code: '452 4.3.1',
|
||||
message: 'Insufficient system storage',
|
||||
type: 'system-disk',
|
||||
retry: true
|
||||
},
|
||||
{
|
||||
code: '452 4.2.0',
|
||||
message: 'Quota exceeded',
|
||||
type: 'generic-quota',
|
||||
retry: true
|
||||
},
|
||||
{
|
||||
code: '422',
|
||||
message: 'Recipient mailbox has exceeded storage limit',
|
||||
type: 'recipient-storage',
|
||||
retry: true
|
||||
}
|
||||
];
|
||||
|
||||
console.log('\nQuota error classification:');
|
||||
|
||||
for (const error of quotaErrors) {
|
||||
console.log(`\n${error.code} ${error.message}`);
|
||||
console.log(` Type: ${error.type}`);
|
||||
console.log(` Retryable: ${error.retry}`);
|
||||
console.log(` Action: ${error.retry ? 'Queue and retry later' : 'Bounce immediately'}`);
|
||||
}
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-05: Quota handling strategies', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
quotaRetryStrategy: 'exponential',
|
||||
quotaMaxRetries: 5,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// Simulate quota tracking
|
||||
const quotaTracker = {
|
||||
recipients: new Map<string, { attempts: number; lastAttempt: number; quotaFull: boolean }>()
|
||||
};
|
||||
|
||||
smtpClient.on('quota-exceeded', (info) => {
|
||||
const recipient = info.recipient;
|
||||
const existing = quotaTracker.recipients.get(recipient) || { attempts: 0, lastAttempt: 0, quotaFull: false };
|
||||
|
||||
existing.attempts++;
|
||||
existing.lastAttempt = Date.now();
|
||||
existing.quotaFull = info.permanent;
|
||||
|
||||
quotaTracker.recipients.set(recipient, existing);
|
||||
|
||||
console.log(`Quota exceeded for ${recipient}: attempt ${existing.attempts}`);
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
// Test batch sending with quota issues
|
||||
const recipients = [
|
||||
'normal1@example.com',
|
||||
'quotafull@example.com',
|
||||
'normal2@example.com',
|
||||
'overquota@example.com',
|
||||
'normal3@example.com'
|
||||
];
|
||||
|
||||
console.log('\nSending batch with quota issues...');
|
||||
|
||||
for (const recipient of recipients) {
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: [recipient],
|
||||
subject: 'Batch quota test',
|
||||
text: 'Testing quota handling in batch'
|
||||
});
|
||||
|
||||
try {
|
||||
await smtpClient.sendMail(email);
|
||||
console.log(`✓ ${recipient}: Sent successfully`);
|
||||
} catch (error) {
|
||||
const quotaInfo = quotaTracker.recipients.get(recipient);
|
||||
|
||||
if (error.message.match(/quota|full|storage/i)) {
|
||||
console.log(`✗ ${recipient}: Quota error (${quotaInfo?.attempts || 1} attempts)`);
|
||||
} else {
|
||||
console.log(`✗ ${recipient}: Other error - ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show quota statistics
|
||||
console.log('\nQuota statistics:');
|
||||
quotaTracker.recipients.forEach((info, recipient) => {
|
||||
console.log(` ${recipient}: ${info.attempts} attempts, ${info.quotaFull ? 'permanent' : 'temporary'} quota issue`);
|
||||
});
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-05: Per-domain quota limits', async () => {
|
||||
// Server with per-domain quotas
|
||||
const domainQuotaServer = net.createServer((socket) => {
|
||||
const domainQuotas: { [domain: string]: { used: number; limit: number } } = {
|
||||
'limited.com': { used: 0, limit: 3 },
|
||||
'premium.com': { used: 0, limit: 100 },
|
||||
'full.com': { used: 100, limit: 100 }
|
||||
};
|
||||
|
||||
socket.write('220 Domain Quota Server\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO')) {
|
||||
const match = command.match(/<[^@]+@([^>]+)>/);
|
||||
if (match) {
|
||||
const domain = match[1];
|
||||
const quota = domainQuotas[domain];
|
||||
|
||||
if (quota) {
|
||||
if (quota.used >= quota.limit) {
|
||||
socket.write(`452 4.2.2 Domain ${domain} quota exceeded (${quota.used}/${quota.limit})\r\n`);
|
||||
} else {
|
||||
quota.used++;
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
}
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Send data\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
domainQuotaServer.listen(0, '127.0.0.1', () => resolve());
|
||||
});
|
||||
|
||||
const domainQuotaPort = (domainQuotaServer.address() as net.AddressInfo).port;
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: domainQuotaPort,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
console.log('\nTesting per-domain quotas...');
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
// Send to different domains
|
||||
const testRecipients = [
|
||||
'user1@limited.com',
|
||||
'user2@limited.com',
|
||||
'user3@limited.com',
|
||||
'user4@limited.com', // Should exceed quota
|
||||
'user1@premium.com',
|
||||
'user1@full.com' // Should fail immediately
|
||||
];
|
||||
|
||||
for (const recipient of testRecipients) {
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: [recipient],
|
||||
subject: 'Domain quota test',
|
||||
text: 'Testing per-domain quotas'
|
||||
});
|
||||
|
||||
try {
|
||||
await smtpClient.sendMail(email);
|
||||
console.log(`✓ ${recipient}: Sent`);
|
||||
} catch (error) {
|
||||
console.log(`✗ ${recipient}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
await smtpClient.close();
|
||||
domainQuotaServer.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-05: Quota warning headers', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
// Send email that might trigger quota warnings
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Quota Warning Test',
|
||||
text: 'x'.repeat(1024 * 1024), // 1MB
|
||||
headers: {
|
||||
'X-Check-Quota': 'yes'
|
||||
}
|
||||
});
|
||||
|
||||
// Monitor for quota-related response headers
|
||||
const responseHeaders: string[] = [];
|
||||
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
|
||||
|
||||
smtpClient.sendCommand = async (command: string) => {
|
||||
const response = await originalSendCommand(command);
|
||||
|
||||
// Check for quota warnings in responses
|
||||
if (response.includes('quota') || response.includes('storage') || response.includes('size')) {
|
||||
responseHeaders.push(response);
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
await smtpClient.sendMail(email);
|
||||
|
||||
console.log('\nQuota-related responses:');
|
||||
responseHeaders.forEach(header => {
|
||||
console.log(` ${header.trim()}`);
|
||||
});
|
||||
|
||||
// Check for quota warning patterns
|
||||
const warningPatterns = [
|
||||
/(\d+)% of quota used/,
|
||||
/(\d+) bytes? remaining/,
|
||||
/quota warning: (\d+)/,
|
||||
/approaching quota limit/
|
||||
];
|
||||
|
||||
responseHeaders.forEach(response => {
|
||||
warningPatterns.forEach(pattern => {
|
||||
const match = response.match(pattern);
|
||||
if (match) {
|
||||
console.log(` Warning detected: ${match[0]}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-05: Quota recovery detection', async () => {
|
||||
// Server that simulates quota recovery
|
||||
let quotaFull = true;
|
||||
let checkCount = 0;
|
||||
|
||||
const recoveryServer = net.createServer((socket) => {
|
||||
socket.write('220 Recovery Test Server\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO')) {
|
||||
checkCount++;
|
||||
|
||||
// Simulate quota recovery after 3 checks
|
||||
if (checkCount > 3) {
|
||||
quotaFull = false;
|
||||
}
|
||||
|
||||
if (quotaFull) {
|
||||
socket.write('452 4.2.2 Mailbox full\r\n');
|
||||
} else {
|
||||
socket.write('250 OK - quota available\r\n');
|
||||
}
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Send data\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
recoveryServer.listen(0, '127.0.0.1', () => resolve());
|
||||
});
|
||||
|
||||
const recoveryPort = (recoveryServer.address() as net.AddressInfo).port;
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: recoveryPort,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
quotaRetryDelay: 1000,
|
||||
quotaRecoveryCheck: true,
|
||||
debug: true
|
||||
});
|
||||
|
||||
console.log('\nTesting quota recovery detection...');
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Quota Recovery Test',
|
||||
text: 'Testing quota recovery'
|
||||
});
|
||||
|
||||
// Try sending with retries
|
||||
let attempts = 0;
|
||||
let success = false;
|
||||
|
||||
while (attempts < 5 && !success) {
|
||||
attempts++;
|
||||
console.log(`\nAttempt ${attempts}:`);
|
||||
|
||||
try {
|
||||
await smtpClient.sendMail(email);
|
||||
success = true;
|
||||
console.log(' Success! Quota recovered');
|
||||
} catch (error) {
|
||||
console.log(` Failed: ${error.message}`);
|
||||
|
||||
if (attempts < 5) {
|
||||
console.log(' Waiting before retry...');
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(success).toBeTruthy();
|
||||
expect(attempts).toBeGreaterThan(3); // Should succeed after quota recovery
|
||||
|
||||
await smtpClient.close();
|
||||
recoveryServer.close();
|
||||
});
|
||||
|
||||
tap.test('cleanup test SMTP server', async () => {
|
||||
if (testServer) {
|
||||
await testServer.stop();
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
@ -0,0 +1,513 @@
|
||||
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();
|
Reference in New Issue
Block a user