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" ' }, { 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' + ': 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();