583 lines
16 KiB
TypeScript
583 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-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(); |