628 lines
19 KiB
TypeScript
628 lines
19 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-10: Partial recipient failure', async () => {
|
||
|
// Create server that accepts some recipients and rejects others
|
||
|
const partialFailureServer = net.createServer((socket) => {
|
||
|
socket.write('220 Partial Failure 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')) {
|
||
|
const recipient = command.match(/<([^>]+)>/)?.[1] || '';
|
||
|
|
||
|
// Accept/reject based on recipient
|
||
|
if (recipient.includes('valid')) {
|
||
|
socket.write('250 OK\r\n');
|
||
|
} else if (recipient.includes('invalid')) {
|
||
|
socket.write('550 5.1.1 User unknown\r\n');
|
||
|
} else if (recipient.includes('full')) {
|
||
|
socket.write('452 4.2.2 Mailbox full\r\n');
|
||
|
} else if (recipient.includes('greylisted')) {
|
||
|
socket.write('451 4.7.1 Greylisted, try again later\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 - delivered to accepted recipients only\r\n');
|
||
|
} else if (command === 'QUIT') {
|
||
|
socket.write('221 Bye\r\n');
|
||
|
socket.end();
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
|
||
|
await new Promise<void>((resolve) => {
|
||
|
partialFailureServer.listen(0, '127.0.0.1', () => resolve());
|
||
|
});
|
||
|
|
||
|
const partialPort = (partialFailureServer.address() as net.AddressInfo).port;
|
||
|
|
||
|
const smtpClient = createSmtpClient({
|
||
|
host: '127.0.0.1',
|
||
|
port: partialPort,
|
||
|
secure: false,
|
||
|
connectionTimeout: 5000,
|
||
|
continueOnRecipientError: true, // Continue even if some recipients fail
|
||
|
debug: true
|
||
|
});
|
||
|
|
||
|
console.log('Testing partial recipient failure...');
|
||
|
|
||
|
await smtpClient.connect();
|
||
|
|
||
|
const email = new Email({
|
||
|
from: 'sender@example.com',
|
||
|
to: [
|
||
|
'valid1@example.com',
|
||
|
'invalid@example.com',
|
||
|
'valid2@example.com',
|
||
|
'full@example.com',
|
||
|
'valid3@example.com',
|
||
|
'greylisted@example.com'
|
||
|
],
|
||
|
subject: 'Partial failure test',
|
||
|
text: 'Testing partial recipient failures'
|
||
|
});
|
||
|
|
||
|
try {
|
||
|
const result = await smtpClient.sendMail(email);
|
||
|
|
||
|
console.log('\nPartial send results:');
|
||
|
console.log(` Total recipients: ${email.to.length}`);
|
||
|
console.log(` Accepted: ${result.accepted?.length || 0}`);
|
||
|
console.log(` Rejected: ${result.rejected?.length || 0}`);
|
||
|
console.log(` Pending: ${result.pending?.length || 0}`);
|
||
|
|
||
|
if (result.accepted && result.accepted.length > 0) {
|
||
|
console.log('\nAccepted recipients:');
|
||
|
result.accepted.forEach(r => console.log(` ✓ ${r}`));
|
||
|
}
|
||
|
|
||
|
if (result.rejected && result.rejected.length > 0) {
|
||
|
console.log('\nRejected recipients:');
|
||
|
result.rejected.forEach(r => console.log(` ✗ ${r.recipient}: ${r.reason}`));
|
||
|
}
|
||
|
|
||
|
if (result.pending && result.pending.length > 0) {
|
||
|
console.log('\nPending recipients (temporary failures):');
|
||
|
result.pending.forEach(r => console.log(` ⏳ ${r.recipient}: ${r.reason}`));
|
||
|
}
|
||
|
|
||
|
// Should have partial success
|
||
|
expect(result.accepted?.length).toBeGreaterThan(0);
|
||
|
expect(result.rejected?.length).toBeGreaterThan(0);
|
||
|
|
||
|
} catch (error) {
|
||
|
console.log('Unexpected complete failure:', error.message);
|
||
|
}
|
||
|
|
||
|
await smtpClient.close();
|
||
|
partialFailureServer.close();
|
||
|
});
|
||
|
|
||
|
tap.test('CERR-10: Partial failure policies', async () => {
|
||
|
const smtpClient = createSmtpClient({
|
||
|
host: testServer.hostname,
|
||
|
port: testServer.port,
|
||
|
secure: false,
|
||
|
connectionTimeout: 5000,
|
||
|
debug: true
|
||
|
});
|
||
|
|
||
|
await smtpClient.connect();
|
||
|
|
||
|
console.log('\nTesting different partial failure policies:');
|
||
|
|
||
|
// Policy configurations
|
||
|
const policies = [
|
||
|
{
|
||
|
name: 'Fail if any recipient fails',
|
||
|
continueOnError: false,
|
||
|
minSuccessRate: 1.0
|
||
|
},
|
||
|
{
|
||
|
name: 'Continue if any recipient succeeds',
|
||
|
continueOnError: true,
|
||
|
minSuccessRate: 0.01
|
||
|
},
|
||
|
{
|
||
|
name: 'Require 50% success rate',
|
||
|
continueOnError: true,
|
||
|
minSuccessRate: 0.5
|
||
|
},
|
||
|
{
|
||
|
name: 'Require at least 2 recipients',
|
||
|
continueOnError: true,
|
||
|
minSuccessCount: 2
|
||
|
}
|
||
|
];
|
||
|
|
||
|
for (const policy of policies) {
|
||
|
console.log(`\n${policy.name}:`);
|
||
|
console.log(` Continue on error: ${policy.continueOnError}`);
|
||
|
if (policy.minSuccessRate !== undefined) {
|
||
|
console.log(` Min success rate: ${(policy.minSuccessRate * 100).toFixed(0)}%`);
|
||
|
}
|
||
|
if (policy.minSuccessCount !== undefined) {
|
||
|
console.log(` Min success count: ${policy.minSuccessCount}`);
|
||
|
}
|
||
|
|
||
|
// Simulate applying policy
|
||
|
const results = {
|
||
|
accepted: ['user1@example.com', 'user2@example.com'],
|
||
|
rejected: ['invalid@example.com'],
|
||
|
total: 3
|
||
|
};
|
||
|
|
||
|
const successRate = results.accepted.length / results.total;
|
||
|
let shouldProceed = policy.continueOnError;
|
||
|
|
||
|
if (policy.minSuccessRate !== undefined) {
|
||
|
shouldProceed = shouldProceed && (successRate >= policy.minSuccessRate);
|
||
|
}
|
||
|
|
||
|
if (policy.minSuccessCount !== undefined) {
|
||
|
shouldProceed = shouldProceed && (results.accepted.length >= policy.minSuccessCount);
|
||
|
}
|
||
|
|
||
|
console.log(` With ${results.accepted.length}/${results.total} success: ${shouldProceed ? 'PROCEED' : 'FAIL'}`);
|
||
|
}
|
||
|
|
||
|
await smtpClient.close();
|
||
|
});
|
||
|
|
||
|
tap.test('CERR-10: Partial data transmission failure', async () => {
|
||
|
// Server that fails during DATA phase
|
||
|
const dataFailureServer = net.createServer((socket) => {
|
||
|
let dataSize = 0;
|
||
|
let inData = false;
|
||
|
|
||
|
socket.write('220 Data Failure Test Server\r\n');
|
||
|
|
||
|
socket.on('data', (data) => {
|
||
|
const command = data.toString();
|
||
|
|
||
|
if (command.trim().startsWith('EHLO')) {
|
||
|
socket.write('250 OK\r\n');
|
||
|
} else if (command.trim().startsWith('MAIL FROM')) {
|
||
|
socket.write('250 OK\r\n');
|
||
|
} else if (command.trim().startsWith('RCPT TO')) {
|
||
|
socket.write('250 OK\r\n');
|
||
|
} else if (command.trim() === 'DATA') {
|
||
|
inData = true;
|
||
|
dataSize = 0;
|
||
|
socket.write('354 Send data\r\n');
|
||
|
} else if (inData) {
|
||
|
dataSize += data.length;
|
||
|
|
||
|
// Fail after receiving 1KB of data
|
||
|
if (dataSize > 1024) {
|
||
|
socket.write('451 4.3.0 Message transmission failed\r\n');
|
||
|
socket.destroy();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (command.includes('\r\n.\r\n')) {
|
||
|
inData = false;
|
||
|
socket.write('250 OK\r\n');
|
||
|
}
|
||
|
} else if (command.trim() === 'QUIT') {
|
||
|
socket.write('221 Bye\r\n');
|
||
|
socket.end();
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
|
||
|
await new Promise<void>((resolve) => {
|
||
|
dataFailureServer.listen(0, '127.0.0.1', () => resolve());
|
||
|
});
|
||
|
|
||
|
const dataFailurePort = (dataFailureServer.address() as net.AddressInfo).port;
|
||
|
|
||
|
const smtpClient = createSmtpClient({
|
||
|
host: '127.0.0.1',
|
||
|
port: dataFailurePort,
|
||
|
secure: false,
|
||
|
connectionTimeout: 5000,
|
||
|
debug: true
|
||
|
});
|
||
|
|
||
|
console.log('\nTesting partial data transmission failure...');
|
||
|
|
||
|
await smtpClient.connect();
|
||
|
|
||
|
// Try to send large message that will fail during transmission
|
||
|
const largeEmail = new Email({
|
||
|
from: 'sender@example.com',
|
||
|
to: ['recipient@example.com'],
|
||
|
subject: 'Large message test',
|
||
|
text: 'x'.repeat(2048) // 2KB - will fail after 1KB
|
||
|
});
|
||
|
|
||
|
try {
|
||
|
await smtpClient.sendMail(largeEmail);
|
||
|
console.log('Unexpected success');
|
||
|
} catch (error) {
|
||
|
console.log('Data transmission failed as expected:', error.message);
|
||
|
expect(error.message).toMatch(/451|transmission|failed/i);
|
||
|
}
|
||
|
|
||
|
// Try smaller message that should succeed
|
||
|
const smallEmail = new Email({
|
||
|
from: 'sender@example.com',
|
||
|
to: ['recipient@example.com'],
|
||
|
subject: 'Small message test',
|
||
|
text: 'This is a small message'
|
||
|
});
|
||
|
|
||
|
// Need new connection after failure
|
||
|
await smtpClient.close();
|
||
|
await smtpClient.connect();
|
||
|
|
||
|
try {
|
||
|
await smtpClient.sendMail(smallEmail);
|
||
|
console.log('Small message sent successfully');
|
||
|
} catch (error) {
|
||
|
console.log('Small message also failed:', error.message);
|
||
|
}
|
||
|
|
||
|
await smtpClient.close();
|
||
|
dataFailureServer.close();
|
||
|
});
|
||
|
|
||
|
tap.test('CERR-10: Partial failure recovery strategies', async () => {
|
||
|
const smtpClient = createSmtpClient({
|
||
|
host: testServer.hostname,
|
||
|
port: testServer.port,
|
||
|
secure: false,
|
||
|
connectionTimeout: 5000,
|
||
|
partialFailureStrategy: 'retry-failed',
|
||
|
debug: true
|
||
|
});
|
||
|
|
||
|
await smtpClient.connect();
|
||
|
|
||
|
console.log('\nPartial failure recovery strategies:');
|
||
|
|
||
|
const strategies = [
|
||
|
{
|
||
|
name: 'Retry failed recipients',
|
||
|
description: 'Queue failed recipients for retry',
|
||
|
implementation: async (result: any) => {
|
||
|
if (result.rejected && result.rejected.length > 0) {
|
||
|
console.log(` Queueing ${result.rejected.length} recipients for retry`);
|
||
|
// Would implement retry queue here
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
name: 'Bounce failed recipients',
|
||
|
description: 'Send bounce notifications immediately',
|
||
|
implementation: async (result: any) => {
|
||
|
if (result.rejected && result.rejected.length > 0) {
|
||
|
console.log(` Generating bounce messages for ${result.rejected.length} recipients`);
|
||
|
// Would generate NDR here
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
name: 'Split and retry',
|
||
|
description: 'Split into individual messages',
|
||
|
implementation: async (result: any) => {
|
||
|
if (result.rejected && result.rejected.length > 0) {
|
||
|
console.log(` Splitting into ${result.rejected.length} individual messages`);
|
||
|
// Would send individual messages here
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
{
|
||
|
name: 'Fallback transport',
|
||
|
description: 'Try alternative delivery method',
|
||
|
implementation: async (result: any) => {
|
||
|
if (result.rejected && result.rejected.length > 0) {
|
||
|
console.log(` Attempting fallback delivery for ${result.rejected.length} recipients`);
|
||
|
// Would try alternative server/route here
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
];
|
||
|
|
||
|
// Simulate partial failure
|
||
|
const mockResult = {
|
||
|
accepted: ['user1@example.com', 'user2@example.com'],
|
||
|
rejected: [
|
||
|
{ recipient: 'invalid@example.com', reason: '550 User unknown' },
|
||
|
{ recipient: 'full@example.com', reason: '552 Mailbox full' }
|
||
|
],
|
||
|
pending: [
|
||
|
{ recipient: 'greylisted@example.com', reason: '451 Greylisted' }
|
||
|
]
|
||
|
};
|
||
|
|
||
|
for (const strategy of strategies) {
|
||
|
console.log(`\n${strategy.name}:`);
|
||
|
console.log(` Description: ${strategy.description}`);
|
||
|
await strategy.implementation(mockResult);
|
||
|
}
|
||
|
|
||
|
await smtpClient.close();
|
||
|
});
|
||
|
|
||
|
tap.test('CERR-10: Transaction state after partial failure', async () => {
|
||
|
const smtpClient = createSmtpClient({
|
||
|
host: testServer.hostname,
|
||
|
port: testServer.port,
|
||
|
secure: false,
|
||
|
connectionTimeout: 5000,
|
||
|
debug: true
|
||
|
});
|
||
|
|
||
|
await smtpClient.connect();
|
||
|
|
||
|
console.log('\nTesting transaction state after partial failure...');
|
||
|
|
||
|
// Start transaction
|
||
|
await smtpClient.sendCommand('MAIL FROM:<sender@example.com>');
|
||
|
|
||
|
// Add recipients with mixed results
|
||
|
const recipients = [
|
||
|
{ email: 'valid@example.com', shouldSucceed: true },
|
||
|
{ email: 'invalid@nonexistent.com', shouldSucceed: false },
|
||
|
{ email: 'another@example.com', shouldSucceed: true }
|
||
|
];
|
||
|
|
||
|
const results = [];
|
||
|
|
||
|
for (const recipient of recipients) {
|
||
|
try {
|
||
|
const response = await smtpClient.sendCommand(`RCPT TO:<${recipient.email}>`);
|
||
|
results.push({
|
||
|
email: recipient.email,
|
||
|
success: response.startsWith('250'),
|
||
|
response: response.trim()
|
||
|
});
|
||
|
} catch (error) {
|
||
|
results.push({
|
||
|
email: recipient.email,
|
||
|
success: false,
|
||
|
response: error.message
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
console.log('\nRecipient results:');
|
||
|
results.forEach(r => {
|
||
|
console.log(` ${r.email}: ${r.success ? '✓' : '✗'} ${r.response}`);
|
||
|
});
|
||
|
|
||
|
const acceptedCount = results.filter(r => r.success).length;
|
||
|
|
||
|
if (acceptedCount > 0) {
|
||
|
console.log(`\n${acceptedCount} recipients accepted, proceeding with DATA...`);
|
||
|
|
||
|
try {
|
||
|
const dataResponse = await smtpClient.sendCommand('DATA');
|
||
|
console.log('DATA response:', dataResponse.trim());
|
||
|
|
||
|
if (dataResponse.startsWith('354')) {
|
||
|
await smtpClient.sendCommand('Subject: Partial recipient test\r\n\r\nTest message\r\n.');
|
||
|
console.log('Message sent to accepted recipients');
|
||
|
}
|
||
|
} catch (error) {
|
||
|
console.log('DATA phase error:', error.message);
|
||
|
}
|
||
|
} else {
|
||
|
console.log('\nNo recipients accepted, resetting transaction');
|
||
|
await smtpClient.sendCommand('RSET');
|
||
|
}
|
||
|
|
||
|
await smtpClient.close();
|
||
|
});
|
||
|
|
||
|
tap.test('CERR-10: Partial authentication failure', async () => {
|
||
|
// Server with selective authentication
|
||
|
const authFailureServer = net.createServer((socket) => {
|
||
|
socket.write('220 Auth Failure Test Server\r\n');
|
||
|
|
||
|
socket.on('data', (data) => {
|
||
|
const command = data.toString().trim();
|
||
|
|
||
|
if (command.startsWith('EHLO')) {
|
||
|
socket.write('250-authfailure.example.com\r\n');
|
||
|
socket.write('250-AUTH PLAIN LOGIN\r\n');
|
||
|
socket.write('250 OK\r\n');
|
||
|
} else if (command.startsWith('AUTH')) {
|
||
|
// Randomly fail authentication
|
||
|
if (Math.random() > 0.5) {
|
||
|
socket.write('235 2.7.0 Authentication successful\r\n');
|
||
|
} else {
|
||
|
socket.write('535 5.7.8 Authentication credentials invalid\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) => {
|
||
|
authFailureServer.listen(0, '127.0.0.1', () => resolve());
|
||
|
});
|
||
|
|
||
|
const authPort = (authFailureServer.address() as net.AddressInfo).port;
|
||
|
|
||
|
console.log('\nTesting partial authentication failure with fallback...');
|
||
|
|
||
|
// Try multiple authentication methods
|
||
|
const authMethods = [
|
||
|
{ method: 'PLAIN', credentials: 'user1:pass1' },
|
||
|
{ method: 'LOGIN', credentials: 'user2:pass2' },
|
||
|
{ method: 'PLAIN', credentials: 'user3:pass3' }
|
||
|
];
|
||
|
|
||
|
let authenticated = false;
|
||
|
let attempts = 0;
|
||
|
|
||
|
for (const auth of authMethods) {
|
||
|
attempts++;
|
||
|
|
||
|
const smtpClient = createSmtpClient({
|
||
|
host: '127.0.0.1',
|
||
|
port: authPort,
|
||
|
secure: false,
|
||
|
auth: {
|
||
|
method: auth.method,
|
||
|
user: auth.credentials.split(':')[0],
|
||
|
pass: auth.credentials.split(':')[1]
|
||
|
},
|
||
|
connectionTimeout: 5000,
|
||
|
debug: true
|
||
|
});
|
||
|
|
||
|
console.log(`\nAttempt ${attempts}: ${auth.method} authentication`);
|
||
|
|
||
|
try {
|
||
|
await smtpClient.connect();
|
||
|
authenticated = true;
|
||
|
console.log('Authentication successful');
|
||
|
|
||
|
// Send test message
|
||
|
await smtpClient.sendMail(new Email({
|
||
|
from: 'sender@example.com',
|
||
|
to: ['recipient@example.com'],
|
||
|
subject: 'Auth test',
|
||
|
text: 'Successfully authenticated'
|
||
|
}));
|
||
|
|
||
|
await smtpClient.close();
|
||
|
break;
|
||
|
} catch (error) {
|
||
|
console.log('Authentication failed:', error.message);
|
||
|
await smtpClient.close();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
console.log(`\nAuthentication ${authenticated ? 'succeeded' : 'failed'} after ${attempts} attempts`);
|
||
|
|
||
|
authFailureServer.close();
|
||
|
});
|
||
|
|
||
|
tap.test('CERR-10: Partial failure reporting', async () => {
|
||
|
const smtpClient = createSmtpClient({
|
||
|
host: testServer.hostname,
|
||
|
port: testServer.port,
|
||
|
secure: false,
|
||
|
connectionTimeout: 5000,
|
||
|
generatePartialFailureReport: true,
|
||
|
debug: true
|
||
|
});
|
||
|
|
||
|
await smtpClient.connect();
|
||
|
|
||
|
console.log('\nGenerating partial failure report...');
|
||
|
|
||
|
// Simulate partial failure result
|
||
|
const partialResult = {
|
||
|
messageId: '<123456@example.com>',
|
||
|
timestamp: new Date(),
|
||
|
from: 'sender@example.com',
|
||
|
accepted: [
|
||
|
'user1@example.com',
|
||
|
'user2@example.com',
|
||
|
'user3@example.com'
|
||
|
],
|
||
|
rejected: [
|
||
|
{ recipient: 'invalid@example.com', code: '550', reason: 'User unknown' },
|
||
|
{ recipient: 'full@example.com', code: '552', reason: 'Mailbox full' }
|
||
|
],
|
||
|
pending: [
|
||
|
{ recipient: 'grey@example.com', code: '451', reason: 'Greylisted' }
|
||
|
]
|
||
|
};
|
||
|
|
||
|
// Generate failure report
|
||
|
const report = {
|
||
|
summary: {
|
||
|
total: partialResult.accepted.length + partialResult.rejected.length + partialResult.pending.length,
|
||
|
delivered: partialResult.accepted.length,
|
||
|
failed: partialResult.rejected.length,
|
||
|
deferred: partialResult.pending.length,
|
||
|
successRate: ((partialResult.accepted.length / (partialResult.accepted.length + partialResult.rejected.length + partialResult.pending.length)) * 100).toFixed(1)
|
||
|
},
|
||
|
details: {
|
||
|
messageId: partialResult.messageId,
|
||
|
timestamp: partialResult.timestamp.toISOString(),
|
||
|
from: partialResult.from,
|
||
|
recipients: {
|
||
|
delivered: partialResult.accepted,
|
||
|
failed: partialResult.rejected.map(r => ({
|
||
|
address: r.recipient,
|
||
|
error: `${r.code} ${r.reason}`,
|
||
|
permanent: r.code.startsWith('5')
|
||
|
})),
|
||
|
deferred: partialResult.pending.map(r => ({
|
||
|
address: r.recipient,
|
||
|
error: `${r.code} ${r.reason}`,
|
||
|
retryAfter: new Date(Date.now() + 300000).toISOString() // 5 minutes
|
||
|
}))
|
||
|
}
|
||
|
},
|
||
|
actions: {
|
||
|
failed: 'Generate bounce notifications',
|
||
|
deferred: 'Queue for retry in 5 minutes'
|
||
|
}
|
||
|
};
|
||
|
|
||
|
console.log('\nPartial Failure Report:');
|
||
|
console.log(JSON.stringify(report, null, 2));
|
||
|
|
||
|
// Send notification email about partial failure
|
||
|
const notificationEmail = new Email({
|
||
|
from: 'postmaster@example.com',
|
||
|
to: ['sender@example.com'],
|
||
|
subject: 'Partial delivery failure',
|
||
|
text: `Your message ${partialResult.messageId} was partially delivered.\n\n` +
|
||
|
`Delivered: ${report.summary.delivered}\n` +
|
||
|
`Failed: ${report.summary.failed}\n` +
|
||
|
`Deferred: ${report.summary.deferred}\n` +
|
||
|
`Success rate: ${report.summary.successRate}%`
|
||
|
});
|
||
|
|
||
|
try {
|
||
|
await smtpClient.sendMail(notificationEmail);
|
||
|
console.log('\nPartial failure notification sent');
|
||
|
} catch (error) {
|
||
|
console.log('Failed to send notification:', error.message);
|
||
|
}
|
||
|
|
||
|
await smtpClient.close();
|
||
|
});
|
||
|
|
||
|
tap.test('cleanup test SMTP server', async () => {
|
||
|
if (testServer) {
|
||
|
await testServer.stop();
|
||
|
}
|
||
|
});
|
||
|
|
||
|
export default tap.start();
|