573 lines
16 KiB
TypeScript
573 lines
16 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';
|
|||
|
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-08: Connection rate limiting', async () => {
|
|||
|
// Create server with connection rate limiting
|
|||
|
let connectionCount = 0;
|
|||
|
let connectionTimes: number[] = [];
|
|||
|
const maxConnectionsPerMinute = 10;
|
|||
|
|
|||
|
const rateLimitServer = net.createServer((socket) => {
|
|||
|
const now = Date.now();
|
|||
|
connectionTimes.push(now);
|
|||
|
connectionCount++;
|
|||
|
|
|||
|
// Remove old connection times (older than 1 minute)
|
|||
|
connectionTimes = connectionTimes.filter(time => now - time < 60000);
|
|||
|
|
|||
|
if (connectionTimes.length > maxConnectionsPerMinute) {
|
|||
|
socket.write('421 4.7.0 Too many connections, please try again later\r\n');
|
|||
|
socket.end();
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
socket.write('220 Rate Limit 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 === 'QUIT') {
|
|||
|
socket.write('221 Bye\r\n');
|
|||
|
socket.end();
|
|||
|
} else {
|
|||
|
socket.write('250 OK\r\n');
|
|||
|
}
|
|||
|
});
|
|||
|
});
|
|||
|
|
|||
|
await new Promise<void>((resolve) => {
|
|||
|
rateLimitServer.listen(0, '127.0.0.1', () => resolve());
|
|||
|
});
|
|||
|
|
|||
|
const rateLimitPort = (rateLimitServer.address() as net.AddressInfo).port;
|
|||
|
|
|||
|
console.log('\nTesting connection rate limiting...');
|
|||
|
console.log(`Server limit: ${maxConnectionsPerMinute} connections per minute`);
|
|||
|
|
|||
|
// Try to make many connections rapidly
|
|||
|
const connections: any[] = [];
|
|||
|
let accepted = 0;
|
|||
|
let rejected = 0;
|
|||
|
|
|||
|
for (let i = 0; i < 15; i++) {
|
|||
|
try {
|
|||
|
const client = createSmtpClient({
|
|||
|
host: '127.0.0.1',
|
|||
|
port: rateLimitPort,
|
|||
|
secure: false,
|
|||
|
connectionTimeout: 2000,
|
|||
|
debug: false
|
|||
|
});
|
|||
|
|
|||
|
await client.connect();
|
|||
|
accepted++;
|
|||
|
connections.push(client);
|
|||
|
console.log(` Connection ${i + 1}: Accepted`);
|
|||
|
} catch (error) {
|
|||
|
rejected++;
|
|||
|
console.log(` Connection ${i + 1}: Rejected - ${error.message}`);
|
|||
|
expect(error.message).toMatch(/421|too many|rate/i);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
console.log(`\nResults: ${accepted} accepted, ${rejected} rejected`);
|
|||
|
expect(rejected).toBeGreaterThan(0); // Some should be rate limited
|
|||
|
|
|||
|
// Clean up connections
|
|||
|
for (const client of connections) {
|
|||
|
await client.close();
|
|||
|
}
|
|||
|
|
|||
|
rateLimitServer.close();
|
|||
|
});
|
|||
|
|
|||
|
tap.test('CERR-08: Message rate limiting', async () => {
|
|||
|
// Create server with message rate limiting
|
|||
|
const messageRateLimits: { [key: string]: { count: number; resetTime: number } } = {};
|
|||
|
const messagesPerHour = 100;
|
|||
|
|
|||
|
const messageRateLimitServer = net.createServer((socket) => {
|
|||
|
let senderAddress = '';
|
|||
|
|
|||
|
socket.write('220 Message Rate Limit 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')) {
|
|||
|
const match = command.match(/<([^>]+)>/);
|
|||
|
if (match) {
|
|||
|
senderAddress = match[1];
|
|||
|
const now = Date.now();
|
|||
|
|
|||
|
if (!messageRateLimits[senderAddress]) {
|
|||
|
messageRateLimits[senderAddress] = { count: 0, resetTime: now + 3600000 };
|
|||
|
}
|
|||
|
|
|||
|
// Reset if hour has passed
|
|||
|
if (now > messageRateLimits[senderAddress].resetTime) {
|
|||
|
messageRateLimits[senderAddress] = { count: 0, resetTime: now + 3600000 };
|
|||
|
}
|
|||
|
|
|||
|
messageRateLimits[senderAddress].count++;
|
|||
|
|
|||
|
if (messageRateLimits[senderAddress].count > messagesPerHour) {
|
|||
|
socket.write(`421 4.7.0 Message rate limit exceeded (${messagesPerHour}/hour)\r\n`);
|
|||
|
return;
|
|||
|
}
|
|||
|
}
|
|||
|
socket.write('250 OK\r\n');
|
|||
|
} else if (command.startsWith('RCPT TO')) {
|
|||
|
socket.write('250 OK\r\n');
|
|||
|
} else if (command === 'DATA') {
|
|||
|
socket.write('354 Send data\r\n');
|
|||
|
} else if (command === '.') {
|
|||
|
const remaining = messagesPerHour - messageRateLimits[senderAddress].count;
|
|||
|
socket.write(`250 OK (${remaining} messages remaining this hour)\r\n`);
|
|||
|
} else if (command === 'QUIT') {
|
|||
|
socket.write('221 Bye\r\n');
|
|||
|
socket.end();
|
|||
|
}
|
|||
|
});
|
|||
|
});
|
|||
|
|
|||
|
await new Promise<void>((resolve) => {
|
|||
|
messageRateLimitServer.listen(0, '127.0.0.1', () => resolve());
|
|||
|
});
|
|||
|
|
|||
|
const messageRateLimitPort = (messageRateLimitServer.address() as net.AddressInfo).port;
|
|||
|
|
|||
|
const smtpClient = createSmtpClient({
|
|||
|
host: '127.0.0.1',
|
|||
|
port: messageRateLimitPort,
|
|||
|
secure: false,
|
|||
|
connectionTimeout: 5000,
|
|||
|
debug: true
|
|||
|
});
|
|||
|
|
|||
|
console.log('\nTesting message rate limiting...');
|
|||
|
console.log(`Server limit: ${messagesPerHour} messages per hour per sender`);
|
|||
|
|
|||
|
await smtpClient.connect();
|
|||
|
|
|||
|
// Simulate sending many messages
|
|||
|
const testMessageCount = 10;
|
|||
|
const sender = 'bulk-sender@example.com';
|
|||
|
|
|||
|
for (let i = 0; i < testMessageCount; i++) {
|
|||
|
const email = new Email({
|
|||
|
from: sender,
|
|||
|
to: [`recipient${i}@example.com`],
|
|||
|
subject: `Test message ${i + 1}`,
|
|||
|
text: 'Testing message rate limits'
|
|||
|
});
|
|||
|
|
|||
|
try {
|
|||
|
const result = await smtpClient.sendMail(email);
|
|||
|
|
|||
|
// Extract remaining count from response
|
|||
|
const remainingMatch = result.response?.match(/(\d+) messages remaining/);
|
|||
|
if (remainingMatch) {
|
|||
|
console.log(` Message ${i + 1}: Sent (${remainingMatch[1]} remaining)`);
|
|||
|
} else {
|
|||
|
console.log(` Message ${i + 1}: Sent`);
|
|||
|
}
|
|||
|
} catch (error) {
|
|||
|
console.log(` Message ${i + 1}: Rate limited - ${error.message}`);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
await smtpClient.close();
|
|||
|
messageRateLimitServer.close();
|
|||
|
});
|
|||
|
|
|||
|
tap.test('CERR-08: Recipient rate limiting', async () => {
|
|||
|
const smtpClient = createSmtpClient({
|
|||
|
host: testServer.hostname,
|
|||
|
port: testServer.port,
|
|||
|
secure: false,
|
|||
|
connectionTimeout: 5000,
|
|||
|
debug: true
|
|||
|
});
|
|||
|
|
|||
|
await smtpClient.connect();
|
|||
|
|
|||
|
console.log('\nTesting recipient rate limiting...');
|
|||
|
|
|||
|
// Test different recipient rate limit scenarios
|
|||
|
const recipientTests = [
|
|||
|
{
|
|||
|
name: 'Many recipients in single message',
|
|||
|
recipients: Array.from({ length: 200 }, (_, i) => `user${i}@example.com`),
|
|||
|
expectedLimit: 100
|
|||
|
},
|
|||
|
{
|
|||
|
name: 'Rapid sequential messages',
|
|||
|
recipients: Array.from({ length: 50 }, (_, i) => `rapid${i}@example.com`),
|
|||
|
delay: 0
|
|||
|
}
|
|||
|
];
|
|||
|
|
|||
|
for (const test of recipientTests) {
|
|||
|
console.log(`\n${test.name}:`);
|
|||
|
|
|||
|
const email = new Email({
|
|||
|
from: 'sender@example.com',
|
|||
|
to: test.recipients,
|
|||
|
subject: test.name,
|
|||
|
text: 'Testing recipient limits'
|
|||
|
});
|
|||
|
|
|||
|
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++;
|
|||
|
|
|||
|
if (response.match(/rate|limit|too many|slow down/i)) {
|
|||
|
console.log(` Rate limit hit after ${acceptedCount} recipients`);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
return response;
|
|||
|
};
|
|||
|
|
|||
|
try {
|
|||
|
await smtpClient.sendMail(email);
|
|||
|
console.log(` All ${acceptedCount} recipients accepted`);
|
|||
|
} catch (error) {
|
|||
|
console.log(` Accepted: ${acceptedCount}, Rejected: ${rejectedCount}`);
|
|||
|
console.log(` Error: ${error.message}`);
|
|||
|
}
|
|||
|
|
|||
|
await smtpClient.sendCommand('RSET');
|
|||
|
}
|
|||
|
|
|||
|
await smtpClient.close();
|
|||
|
});
|
|||
|
|
|||
|
tap.test('CERR-08: Rate limit response codes', async () => {
|
|||
|
console.log('\nCommon rate limiting response codes:');
|
|||
|
|
|||
|
const rateLimitCodes = [
|
|||
|
{
|
|||
|
code: '421 4.7.0',
|
|||
|
message: 'Too many connections',
|
|||
|
type: 'Connection rate limit',
|
|||
|
action: 'Close connection, retry later'
|
|||
|
},
|
|||
|
{
|
|||
|
code: '450 4.7.1',
|
|||
|
message: 'Rate limit exceeded, try again later',
|
|||
|
type: 'Command rate limit',
|
|||
|
action: 'Temporary failure, queue and retry'
|
|||
|
},
|
|||
|
{
|
|||
|
code: '451 4.7.1',
|
|||
|
message: 'Please slow down',
|
|||
|
type: 'Throttling request',
|
|||
|
action: 'Add delay before next command'
|
|||
|
},
|
|||
|
{
|
|||
|
code: '452 4.5.3',
|
|||
|
message: 'Too many recipients',
|
|||
|
type: 'Recipient limit',
|
|||
|
action: 'Split into multiple messages'
|
|||
|
},
|
|||
|
{
|
|||
|
code: '454 4.7.0',
|
|||
|
message: 'Temporary authentication failure',
|
|||
|
type: 'Auth rate limit',
|
|||
|
action: 'Delay and retry authentication'
|
|||
|
},
|
|||
|
{
|
|||
|
code: '550 5.7.1',
|
|||
|
message: 'Daily sending quota exceeded',
|
|||
|
type: 'Hard quota limit',
|
|||
|
action: 'Stop sending until quota resets'
|
|||
|
}
|
|||
|
];
|
|||
|
|
|||
|
rateLimitCodes.forEach(limit => {
|
|||
|
console.log(`\n${limit.code} ${limit.message}`);
|
|||
|
console.log(` Type: ${limit.type}`);
|
|||
|
console.log(` Action: ${limit.action}`);
|
|||
|
});
|
|||
|
});
|
|||
|
|
|||
|
tap.test('CERR-08: Adaptive rate limiting', async () => {
|
|||
|
const smtpClient = createSmtpClient({
|
|||
|
host: testServer.hostname,
|
|||
|
port: testServer.port,
|
|||
|
secure: false,
|
|||
|
connectionTimeout: 5000,
|
|||
|
adaptiveRateLimit: true,
|
|||
|
initialDelay: 100, // Start with 100ms between commands
|
|||
|
maxDelay: 5000, // Max 5 seconds between commands
|
|||
|
debug: true
|
|||
|
});
|
|||
|
|
|||
|
await smtpClient.connect();
|
|||
|
|
|||
|
console.log('\nTesting adaptive rate limiting...');
|
|||
|
|
|||
|
// Track delays
|
|||
|
const delays: number[] = [];
|
|||
|
let lastCommandTime = Date.now();
|
|||
|
|
|||
|
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
|
|||
|
|
|||
|
smtpClient.sendCommand = async (command: string) => {
|
|||
|
const now = Date.now();
|
|||
|
const delay = now - lastCommandTime;
|
|||
|
delays.push(delay);
|
|||
|
lastCommandTime = now;
|
|||
|
|
|||
|
return originalSendCommand(command);
|
|||
|
};
|
|||
|
|
|||
|
// Send multiple emails and observe delay adaptation
|
|||
|
for (let i = 0; i < 5; i++) {
|
|||
|
const email = new Email({
|
|||
|
from: 'sender@example.com',
|
|||
|
to: [`recipient${i}@example.com`],
|
|||
|
subject: `Adaptive test ${i + 1}`,
|
|||
|
text: 'Testing adaptive rate limiting'
|
|||
|
});
|
|||
|
|
|||
|
try {
|
|||
|
await smtpClient.sendMail(email);
|
|||
|
console.log(` Email ${i + 1}: Sent with ${delays[delays.length - 1]}ms delay`);
|
|||
|
} catch (error) {
|
|||
|
console.log(` Email ${i + 1}: Failed - ${error.message}`);
|
|||
|
|
|||
|
// Check if delay increased
|
|||
|
if (delays.length > 1) {
|
|||
|
const lastDelay = delays[delays.length - 1];
|
|||
|
const previousDelay = delays[delays.length - 2];
|
|||
|
if (lastDelay > previousDelay) {
|
|||
|
console.log(` Delay increased from ${previousDelay}ms to ${lastDelay}ms`);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
await smtpClient.close();
|
|||
|
});
|
|||
|
|
|||
|
tap.test('CERR-08: Rate limit headers', async () => {
|
|||
|
const smtpClient = createSmtpClient({
|
|||
|
host: testServer.hostname,
|
|||
|
port: testServer.port,
|
|||
|
secure: false,
|
|||
|
connectionTimeout: 5000,
|
|||
|
debug: true
|
|||
|
});
|
|||
|
|
|||
|
await smtpClient.connect();
|
|||
|
|
|||
|
console.log('\nChecking for rate limit information in responses...');
|
|||
|
|
|||
|
// Send email and monitor for rate limit headers
|
|||
|
const email = new Email({
|
|||
|
from: 'sender@example.com',
|
|||
|
to: ['recipient@example.com'],
|
|||
|
subject: 'Rate limit header test',
|
|||
|
text: 'Checking for rate limit information'
|
|||
|
});
|
|||
|
|
|||
|
// Monitor responses for rate limit info
|
|||
|
const rateLimitInfo: string[] = [];
|
|||
|
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
|
|||
|
|
|||
|
smtpClient.sendCommand = async (command: string) => {
|
|||
|
const response = await originalSendCommand(command);
|
|||
|
|
|||
|
// Look for rate limit information in responses
|
|||
|
const patterns = [
|
|||
|
/X-RateLimit-Limit: (\d+)/i,
|
|||
|
/X-RateLimit-Remaining: (\d+)/i,
|
|||
|
/X-RateLimit-Reset: (\d+)/i,
|
|||
|
/(\d+) requests? remaining/i,
|
|||
|
/limit.* (\d+) per/i,
|
|||
|
/retry.* (\d+) seconds?/i
|
|||
|
];
|
|||
|
|
|||
|
patterns.forEach(pattern => {
|
|||
|
const match = response.match(pattern);
|
|||
|
if (match) {
|
|||
|
rateLimitInfo.push(match[0]);
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
return response;
|
|||
|
};
|
|||
|
|
|||
|
await smtpClient.sendMail(email);
|
|||
|
|
|||
|
if (rateLimitInfo.length > 0) {
|
|||
|
console.log('Rate limit information found:');
|
|||
|
rateLimitInfo.forEach(info => console.log(` ${info}`));
|
|||
|
} else {
|
|||
|
console.log('No rate limit information in responses');
|
|||
|
}
|
|||
|
|
|||
|
await smtpClient.close();
|
|||
|
});
|
|||
|
|
|||
|
tap.test('CERR-08: Distributed rate limiting', async () => {
|
|||
|
console.log('\nDistributed rate limiting strategies:');
|
|||
|
|
|||
|
const strategies = [
|
|||
|
{
|
|||
|
name: 'Token bucket',
|
|||
|
description: 'Fixed number of tokens replenished at constant rate',
|
|||
|
pros: 'Allows bursts, smooth rate control',
|
|||
|
cons: 'Can be complex to implement distributed'
|
|||
|
},
|
|||
|
{
|
|||
|
name: 'Sliding window',
|
|||
|
description: 'Count requests in moving time window',
|
|||
|
pros: 'More accurate than fixed windows',
|
|||
|
cons: 'Higher memory usage'
|
|||
|
},
|
|||
|
{
|
|||
|
name: 'Fixed window',
|
|||
|
description: 'Reset counter at fixed intervals',
|
|||
|
pros: 'Simple to implement',
|
|||
|
cons: 'Can allow 2x rate at window boundaries'
|
|||
|
},
|
|||
|
{
|
|||
|
name: 'Leaky bucket',
|
|||
|
description: 'Queue with constant drain rate',
|
|||
|
pros: 'Smooth output rate',
|
|||
|
cons: 'Can drop messages if bucket overflows'
|
|||
|
}
|
|||
|
];
|
|||
|
|
|||
|
strategies.forEach(strategy => {
|
|||
|
console.log(`\n${strategy.name}:`);
|
|||
|
console.log(` Description: ${strategy.description}`);
|
|||
|
console.log(` Pros: ${strategy.pros}`);
|
|||
|
console.log(` Cons: ${strategy.cons}`);
|
|||
|
});
|
|||
|
|
|||
|
// Simulate distributed rate limiting
|
|||
|
const distributedLimiter = {
|
|||
|
nodes: ['server1', 'server2', 'server3'],
|
|||
|
globalLimit: 1000, // 1000 messages per minute globally
|
|||
|
perNodeLimit: 400, // Each node can handle 400/min
|
|||
|
currentCounts: { server1: 0, server2: 0, server3: 0 }
|
|||
|
};
|
|||
|
|
|||
|
console.log('\n\nSimulating distributed rate limiting:');
|
|||
|
console.log(`Global limit: ${distributedLimiter.globalLimit}/min`);
|
|||
|
console.log(`Per-node limit: ${distributedLimiter.perNodeLimit}/min`);
|
|||
|
|
|||
|
// Simulate load distribution
|
|||
|
for (let i = 0; i < 20; i++) {
|
|||
|
// Pick least loaded node
|
|||
|
const node = distributedLimiter.nodes.reduce((min, node) =>
|
|||
|
distributedLimiter.currentCounts[node] < distributedLimiter.currentCounts[min] ? node : min
|
|||
|
);
|
|||
|
|
|||
|
distributedLimiter.currentCounts[node]++;
|
|||
|
|
|||
|
if (i % 5 === 4) {
|
|||
|
console.log(`\nAfter ${i + 1} messages:`);
|
|||
|
distributedLimiter.nodes.forEach(n => {
|
|||
|
console.log(` ${n}: ${distributedLimiter.currentCounts[n]} messages`);
|
|||
|
});
|
|||
|
}
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
tap.test('CERR-08: Rate limit bypass strategies', async () => {
|
|||
|
const smtpClient = createSmtpClient({
|
|||
|
host: testServer.hostname,
|
|||
|
port: testServer.port,
|
|||
|
secure: false,
|
|||
|
connectionTimeout: 5000,
|
|||
|
debug: true
|
|||
|
});
|
|||
|
|
|||
|
await smtpClient.connect();
|
|||
|
|
|||
|
console.log('\nLegitimate rate limit management strategies:');
|
|||
|
|
|||
|
// 1. Message batching
|
|||
|
console.log('\n1. Message batching:');
|
|||
|
const recipients = Array.from({ length: 50 }, (_, i) => `user${i}@example.com`);
|
|||
|
const batchSize = 10;
|
|||
|
|
|||
|
for (let i = 0; i < recipients.length; i += batchSize) {
|
|||
|
const batch = recipients.slice(i, i + batchSize);
|
|||
|
const email = new Email({
|
|||
|
from: 'sender@example.com',
|
|||
|
to: batch,
|
|||
|
subject: 'Batched message',
|
|||
|
text: 'Sending in batches to respect rate limits'
|
|||
|
});
|
|||
|
|
|||
|
console.log(` Batch ${Math.floor(i/batchSize) + 1}: ${batch.length} recipients`);
|
|||
|
|
|||
|
try {
|
|||
|
await smtpClient.sendMail(email);
|
|||
|
|
|||
|
// Add delay between batches
|
|||
|
if (i + batchSize < recipients.length) {
|
|||
|
console.log(' Waiting 2 seconds before next batch...');
|
|||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|||
|
}
|
|||
|
} catch (error) {
|
|||
|
console.log(` Batch failed: ${error.message}`);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// 2. Connection pooling with limits
|
|||
|
console.log('\n2. Connection pooling:');
|
|||
|
console.log(' Using multiple connections with per-connection limits');
|
|||
|
console.log(' Example: 5 connections × 20 msg/min = 100 msg/min total');
|
|||
|
|
|||
|
// 3. Retry with backoff
|
|||
|
console.log('\n3. Exponential backoff on rate limits:');
|
|||
|
const backoffDelays = [1, 2, 4, 8, 16, 32];
|
|||
|
backoffDelays.forEach((delay, attempt) => {
|
|||
|
console.log(` Attempt ${attempt + 1}: Wait ${delay} seconds`);
|
|||
|
});
|
|||
|
|
|||
|
await smtpClient.close();
|
|||
|
});
|
|||
|
|
|||
|
tap.test('cleanup test SMTP server', async () => {
|
|||
|
if (testServer) {
|
|||
|
await testServer.stop();
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
export default tap.start();
|