update
This commit is contained in:
@ -0,0 +1,573 @@
|
||||
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({
|
||||
features: ['DSN'] // Enable DSN support
|
||||
});
|
||||
expect(testServer).toBeTruthy();
|
||||
expect(testServer.port).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('CEP-10: Read receipt headers', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
// Create email requesting read receipt
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Important: Please confirm receipt',
|
||||
text: 'Please confirm you have read this message',
|
||||
headers: {
|
||||
'Disposition-Notification-To': 'sender@example.com',
|
||||
'Return-Receipt-To': 'sender@example.com',
|
||||
'X-Confirm-Reading-To': 'sender@example.com',
|
||||
'X-MS-Receipt-Request': 'sender@example.com'
|
||||
}
|
||||
});
|
||||
|
||||
// Monitor receipt headers
|
||||
const receiptHeaders: string[] = [];
|
||||
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
|
||||
|
||||
smtpClient.sendCommand = async (command: string) => {
|
||||
if (command.includes(':') &&
|
||||
(command.toLowerCase().includes('receipt') ||
|
||||
command.toLowerCase().includes('notification') ||
|
||||
command.toLowerCase().includes('confirm'))) {
|
||||
receiptHeaders.push(command.trim());
|
||||
}
|
||||
return originalSendCommand(command);
|
||||
};
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
expect(result).toBeTruthy();
|
||||
|
||||
console.log('Read receipt headers sent:');
|
||||
receiptHeaders.forEach(header => {
|
||||
console.log(` ${header}`);
|
||||
});
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CEP-10: DSN (Delivery Status Notification) requests', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
// Check if server supports DSN
|
||||
const ehloResponse = await smtpClient.sendCommand('EHLO testclient.example.com');
|
||||
const supportsDSN = ehloResponse.includes('DSN');
|
||||
console.log(`Server DSN support: ${supportsDSN}`);
|
||||
|
||||
// Create email with DSN options
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'DSN Test Email',
|
||||
text: 'Testing delivery status notifications',
|
||||
dsn: {
|
||||
notify: ['SUCCESS', 'FAILURE', 'DELAY'],
|
||||
returnType: 'HEADERS',
|
||||
envid: `msg-${Date.now()}`
|
||||
}
|
||||
});
|
||||
|
||||
// Monitor DSN parameters in SMTP commands
|
||||
let mailFromDSN = '';
|
||||
let rcptToDSN = '';
|
||||
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
|
||||
|
||||
smtpClient.sendCommand = async (command: string) => {
|
||||
if (command.startsWith('MAIL FROM')) {
|
||||
mailFromDSN = command;
|
||||
} else if (command.startsWith('RCPT TO')) {
|
||||
rcptToDSN = command;
|
||||
}
|
||||
return originalSendCommand(command);
|
||||
};
|
||||
|
||||
await smtpClient.sendMail(email);
|
||||
|
||||
console.log('\nDSN parameters in SMTP commands:');
|
||||
console.log('MAIL FROM command:', mailFromDSN);
|
||||
console.log('RCPT TO command:', rcptToDSN);
|
||||
|
||||
// Check for DSN parameters
|
||||
if (mailFromDSN.includes('ENVID=')) {
|
||||
console.log(' ✓ ENVID parameter included');
|
||||
}
|
||||
if (rcptToDSN.includes('NOTIFY=')) {
|
||||
console.log(' ✓ NOTIFY parameter included');
|
||||
}
|
||||
if (mailFromDSN.includes('RET=')) {
|
||||
console.log(' ✓ RET parameter included');
|
||||
}
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CEP-10: DSN notify options', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
// Test different DSN notify combinations
|
||||
const notifyOptions = [
|
||||
{ notify: ['SUCCESS'], description: 'Notify on successful delivery only' },
|
||||
{ notify: ['FAILURE'], description: 'Notify on failure only' },
|
||||
{ notify: ['DELAY'], description: 'Notify on delays only' },
|
||||
{ notify: ['SUCCESS', 'FAILURE'], description: 'Notify on success and failure' },
|
||||
{ notify: ['NEVER'], description: 'Never send notifications' },
|
||||
{ notify: [], description: 'Default notification behavior' }
|
||||
];
|
||||
|
||||
for (const option of notifyOptions) {
|
||||
console.log(`\nTesting DSN: ${option.description}`);
|
||||
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: `DSN Test: ${option.description}`,
|
||||
text: 'Testing DSN notify options',
|
||||
dsn: {
|
||||
notify: option.notify as any,
|
||||
returnType: 'HEADERS'
|
||||
}
|
||||
});
|
||||
|
||||
// Monitor RCPT TO command
|
||||
let rcptCommand = '';
|
||||
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
|
||||
|
||||
smtpClient.sendCommand = async (command: string) => {
|
||||
if (command.startsWith('RCPT TO')) {
|
||||
rcptCommand = command;
|
||||
}
|
||||
return originalSendCommand(command);
|
||||
};
|
||||
|
||||
await smtpClient.sendMail(email);
|
||||
|
||||
// Extract NOTIFY parameter
|
||||
const notifyMatch = rcptCommand.match(/NOTIFY=([A-Z,]+)/);
|
||||
if (notifyMatch) {
|
||||
console.log(` NOTIFY parameter: ${notifyMatch[1]}`);
|
||||
} else {
|
||||
console.log(' No NOTIFY parameter (default behavior)');
|
||||
}
|
||||
}
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CEP-10: DSN return types', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
// Test different return types
|
||||
const returnTypes = [
|
||||
{ type: 'FULL', description: 'Return full message on failure' },
|
||||
{ type: 'HEADERS', description: 'Return headers only' }
|
||||
];
|
||||
|
||||
for (const returnType of returnTypes) {
|
||||
console.log(`\nTesting DSN return type: ${returnType.description}`);
|
||||
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: `DSN Return Type: ${returnType.type}`,
|
||||
text: 'Testing DSN return types',
|
||||
dsn: {
|
||||
notify: ['FAILURE'],
|
||||
returnType: returnType.type as 'FULL' | 'HEADERS'
|
||||
}
|
||||
});
|
||||
|
||||
// Monitor MAIL FROM command
|
||||
let mailFromCommand = '';
|
||||
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
|
||||
|
||||
smtpClient.sendCommand = async (command: string) => {
|
||||
if (command.startsWith('MAIL FROM')) {
|
||||
mailFromCommand = command;
|
||||
}
|
||||
return originalSendCommand(command);
|
||||
};
|
||||
|
||||
await smtpClient.sendMail(email);
|
||||
|
||||
// Extract RET parameter
|
||||
const retMatch = mailFromCommand.match(/RET=([A-Z]+)/);
|
||||
if (retMatch) {
|
||||
console.log(` RET parameter: ${retMatch[1]}`);
|
||||
}
|
||||
}
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CEP-10: MDN (Message Disposition Notification)', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
// Create MDN request email
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Please confirm reading',
|
||||
text: 'This message requests a read receipt',
|
||||
headers: {
|
||||
'Disposition-Notification-To': 'sender@example.com',
|
||||
'Disposition-Notification-Options': 'signed-receipt-protocol=optional,pkcs7-signature',
|
||||
'Original-Message-ID': `<${Date.now()}@example.com>`
|
||||
}
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
expect(result).toBeTruthy();
|
||||
|
||||
// Simulate MDN response
|
||||
const mdnResponse = new Email({
|
||||
from: 'recipient@example.com',
|
||||
to: ['sender@example.com'],
|
||||
subject: 'Read: Please confirm reading',
|
||||
headers: {
|
||||
'Content-Type': 'multipart/report; report-type=disposition-notification',
|
||||
'In-Reply-To': `<${Date.now()}@example.com>`,
|
||||
'References': `<${Date.now()}@example.com>`,
|
||||
'Auto-Submitted': 'auto-replied'
|
||||
},
|
||||
text: 'The message was displayed to the recipient',
|
||||
attachments: [{
|
||||
filename: 'disposition-notification.txt',
|
||||
content: Buffer.from(`Reporting-UA: mail.example.com; MailClient/1.0
|
||||
Original-Recipient: rfc822;recipient@example.com
|
||||
Final-Recipient: rfc822;recipient@example.com
|
||||
Original-Message-ID: <${Date.now()}@example.com>
|
||||
Disposition: automatic-action/MDN-sent-automatically; displayed`),
|
||||
contentType: 'message/disposition-notification'
|
||||
}]
|
||||
});
|
||||
|
||||
console.log('\nSimulating MDN response...');
|
||||
await smtpClient.sendMail(mdnResponse);
|
||||
console.log('MDN response sent successfully');
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CEP-10: Multiple recipients with different DSN', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
// Email with multiple recipients, each with different DSN settings
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: [
|
||||
'important@example.com',
|
||||
'normal@example.com',
|
||||
'optional@example.com'
|
||||
],
|
||||
subject: 'Multi-recipient DSN Test',
|
||||
text: 'Testing per-recipient DSN options',
|
||||
dsn: {
|
||||
recipients: {
|
||||
'important@example.com': { notify: ['SUCCESS', 'FAILURE', 'DELAY'] },
|
||||
'normal@example.com': { notify: ['FAILURE'] },
|
||||
'optional@example.com': { notify: ['NEVER'] }
|
||||
},
|
||||
returnType: 'HEADERS'
|
||||
}
|
||||
});
|
||||
|
||||
// Monitor RCPT TO commands
|
||||
const rcptCommands: string[] = [];
|
||||
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
|
||||
|
||||
smtpClient.sendCommand = async (command: string) => {
|
||||
if (command.startsWith('RCPT TO')) {
|
||||
rcptCommands.push(command);
|
||||
}
|
||||
return originalSendCommand(command);
|
||||
};
|
||||
|
||||
await smtpClient.sendMail(email);
|
||||
|
||||
console.log('\nPer-recipient DSN settings:');
|
||||
rcptCommands.forEach(cmd => {
|
||||
const emailMatch = cmd.match(/<([^>]+)>/);
|
||||
const notifyMatch = cmd.match(/NOTIFY=([A-Z,]+)/);
|
||||
if (emailMatch) {
|
||||
console.log(` ${emailMatch[1]}: ${notifyMatch ? notifyMatch[1] : 'default'}`);
|
||||
}
|
||||
});
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CEP-10: DSN with ORCPT', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
// Test ORCPT (Original Recipient) parameter
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['forwarded@example.com'],
|
||||
subject: 'DSN with ORCPT Test',
|
||||
text: 'Testing original recipient tracking',
|
||||
dsn: {
|
||||
notify: ['SUCCESS', 'FAILURE'],
|
||||
returnType: 'HEADERS',
|
||||
orcpt: 'rfc822;original@example.com'
|
||||
}
|
||||
});
|
||||
|
||||
// Monitor RCPT TO command for ORCPT
|
||||
let hasOrcpt = false;
|
||||
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
|
||||
|
||||
smtpClient.sendCommand = async (command: string) => {
|
||||
if (command.includes('ORCPT=')) {
|
||||
hasOrcpt = true;
|
||||
console.log('ORCPT parameter found:', command);
|
||||
}
|
||||
return originalSendCommand(command);
|
||||
};
|
||||
|
||||
await smtpClient.sendMail(email);
|
||||
|
||||
if (!hasOrcpt) {
|
||||
console.log('ORCPT parameter not included (may not be implemented)');
|
||||
}
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CEP-10: Receipt request formats', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
// Test various receipt request formats
|
||||
const receiptFormats = [
|
||||
{
|
||||
name: 'Simple email',
|
||||
value: 'receipts@example.com'
|
||||
},
|
||||
{
|
||||
name: 'With display name',
|
||||
value: '"Receipt Handler" <receipts@example.com>'
|
||||
},
|
||||
{
|
||||
name: 'Multiple addresses',
|
||||
value: 'receipts@example.com, backup@example.com'
|
||||
},
|
||||
{
|
||||
name: 'With comment',
|
||||
value: 'receipts@example.com (Automated System)'
|
||||
}
|
||||
];
|
||||
|
||||
for (const format of receiptFormats) {
|
||||
console.log(`\nTesting receipt format: ${format.name}`);
|
||||
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: `Receipt Format: ${format.name}`,
|
||||
text: 'Testing receipt address formats',
|
||||
headers: {
|
||||
'Disposition-Notification-To': format.value
|
||||
}
|
||||
});
|
||||
|
||||
// Monitor the header
|
||||
let receiptHeader = '';
|
||||
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
|
||||
|
||||
smtpClient.sendCommand = async (command: string) => {
|
||||
if (command.toLowerCase().includes('disposition-notification-to:')) {
|
||||
receiptHeader = command.trim();
|
||||
}
|
||||
return originalSendCommand(command);
|
||||
};
|
||||
|
||||
await smtpClient.sendMail(email);
|
||||
console.log(` Sent as: ${receiptHeader}`);
|
||||
}
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CEP-10: Non-delivery reports', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
// Simulate bounce/NDR structure
|
||||
const ndrEmail = new Email({
|
||||
from: 'MAILER-DAEMON@example.com',
|
||||
to: ['original-sender@example.com'],
|
||||
subject: 'Undelivered Mail Returned to Sender',
|
||||
headers: {
|
||||
'Auto-Submitted': 'auto-replied',
|
||||
'Content-Type': 'multipart/report; report-type=delivery-status',
|
||||
'X-Failed-Recipients': 'nonexistent@example.com'
|
||||
},
|
||||
text: 'This is the mail delivery agent at example.com.\n\n' +
|
||||
'I was unable to deliver your message to the following addresses:\n\n' +
|
||||
'<nonexistent@example.com>: User unknown',
|
||||
attachments: [
|
||||
{
|
||||
filename: 'delivery-status.txt',
|
||||
content: Buffer.from(`Reporting-MTA: dns; mail.example.com
|
||||
X-Queue-ID: 123456789
|
||||
Arrival-Date: ${new Date().toUTCString()}
|
||||
|
||||
Final-Recipient: rfc822;nonexistent@example.com
|
||||
Original-Recipient: rfc822;nonexistent@example.com
|
||||
Action: failed
|
||||
Status: 5.1.1
|
||||
Diagnostic-Code: smtp; 550 5.1.1 User unknown`),
|
||||
contentType: 'message/delivery-status'
|
||||
},
|
||||
{
|
||||
filename: 'original-message.eml',
|
||||
content: Buffer.from('From: original-sender@example.com\r\n' +
|
||||
'To: nonexistent@example.com\r\n' +
|
||||
'Subject: Original Subject\r\n\r\n' +
|
||||
'Original message content'),
|
||||
contentType: 'message/rfc822'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
console.log('\nSimulating Non-Delivery Report (NDR)...');
|
||||
const result = await smtpClient.sendMail(ndrEmail);
|
||||
expect(result).toBeTruthy();
|
||||
console.log('NDR sent successfully');
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CEP-10: Delivery delay notifications', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
// Simulate delayed delivery notification
|
||||
const delayNotification = new Email({
|
||||
from: 'postmaster@example.com',
|
||||
to: ['sender@example.com'],
|
||||
subject: 'Delivery Status: Delayed',
|
||||
headers: {
|
||||
'Auto-Submitted': 'auto-replied',
|
||||
'Content-Type': 'multipart/report; report-type=delivery-status',
|
||||
'X-Delay-Reason': 'Remote server temporarily unavailable'
|
||||
},
|
||||
text: 'This is an automatically generated Delivery Delay Notification.\n\n' +
|
||||
'Your message has not been delivered to the following recipients yet:\n\n' +
|
||||
' recipient@remote-server.com\n\n' +
|
||||
'The server will continue trying to deliver your message for 48 hours.',
|
||||
attachments: [{
|
||||
filename: 'delay-status.txt',
|
||||
content: Buffer.from(`Reporting-MTA: dns; mail.example.com
|
||||
Arrival-Date: ${new Date(Date.now() - 3600000).toUTCString()}
|
||||
Last-Attempt-Date: ${new Date().toUTCString()}
|
||||
|
||||
Final-Recipient: rfc822;recipient@remote-server.com
|
||||
Action: delayed
|
||||
Status: 4.4.1
|
||||
Will-Retry-Until: ${new Date(Date.now() + 172800000).toUTCString()}
|
||||
Diagnostic-Code: smtp; 421 4.4.1 Remote server temporarily unavailable`),
|
||||
contentType: 'message/delivery-status'
|
||||
}]
|
||||
});
|
||||
|
||||
console.log('\nSimulating Delivery Delay Notification...');
|
||||
await smtpClient.sendMail(delayNotification);
|
||||
console.log('Delay notification sent successfully');
|
||||
|
||||
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