feat(email): implement EmailSendJob class for robust email delivery with retry logic and MX record resolution feat(mail): restructure mail module exports for simplified access to core and delivery functionalities
		
			
				
	
	
		
			411 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			411 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import { tap, expect } from '@git.zone/tstest/tapbundle';
 | |
| import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.ts';
 | |
| import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.ts';
 | |
| import { Email } from '../../../ts/mail/core/classes.email.ts';
 | |
| 
 | |
| let testServer: ITestServer;
 | |
| 
 | |
| tap.test('setup test SMTP server', async () => {
 | |
|   testServer = await startTestServer({
 | |
|     port: 2570,
 | |
|     tlsEnabled: false,
 | |
|     authRequired: false
 | |
|   });
 | |
|   expect(testServer).toBeTruthy();
 | |
|   expect(testServer.port).toEqual(2570);
 | |
| });
 | |
| 
 | |
| tap.test('CEP-10: Read receipt headers', async () => {
 | |
|   const smtpClient = await createSmtpClient({
 | |
|     host: testServer.hostname,
 | |
|     port: testServer.port,
 | |
|     secure: false,
 | |
|     connectionTimeout: 5000
 | |
|   });
 | |
| 
 | |
|   // 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'
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   const result = await smtpClient.sendMail(email);
 | |
|   expect(result.success).toBeTruthy();
 | |
|   console.log('Read receipt headers test sent successfully');
 | |
| });
 | |
| 
 | |
| tap.test('CEP-10: DSN (Delivery Status Notification) requests', async () => {
 | |
|   const smtpClient = await createSmtpClient({
 | |
|     host: testServer.hostname,
 | |
|     port: testServer.port,
 | |
|     secure: false,
 | |
|     connectionTimeout: 5000
 | |
|   });
 | |
| 
 | |
|   // 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',
 | |
|     headers: {
 | |
|       'X-DSN-Options': 'notify=SUCCESS,FAILURE,DELAY;return=HEADERS',
 | |
|       'X-Envelope-ID': `msg-${Date.now()}`
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   const result = await smtpClient.sendMail(email);
 | |
|   expect(result.success).toBeTruthy();
 | |
|   console.log('DSN requests test sent successfully');
 | |
| });
 | |
| 
 | |
| tap.test('CEP-10: DSN notify options', async () => {
 | |
|   const smtpClient = await createSmtpClient({
 | |
|     host: testServer.hostname,
 | |
|     port: testServer.port,
 | |
|     secure: false,
 | |
|     connectionTimeout: 5000
 | |
|   });
 | |
| 
 | |
|   // 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' }
 | |
|   ];
 | |
| 
 | |
|   for (const option of notifyOptions) {
 | |
|     console.log(`Testing 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',
 | |
|       headers: {
 | |
|         'X-DSN-Notify': option.notify.join(','),
 | |
|         'X-DSN-Return': 'HEADERS'
 | |
|       }
 | |
|     });
 | |
| 
 | |
|     const result = await smtpClient.sendMail(email);
 | |
|     expect(result.success).toBeTruthy();
 | |
|   }
 | |
|   
 | |
|   console.log('DSN notify options test completed successfully');
 | |
| });
 | |
| 
 | |
| tap.test('CEP-10: DSN return types', async () => {
 | |
|   const smtpClient = await createSmtpClient({
 | |
|     host: testServer.hostname,
 | |
|     port: testServer.port,
 | |
|     secure: false,
 | |
|     connectionTimeout: 5000
 | |
|   });
 | |
| 
 | |
|   // 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(`Testing 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',
 | |
|       headers: {
 | |
|         'X-DSN-Notify': 'FAILURE',
 | |
|         'X-DSN-Return': returnType.type
 | |
|       }
 | |
|     });
 | |
| 
 | |
|     const result = await smtpClient.sendMail(email);
 | |
|     expect(result.success).toBeTruthy();
 | |
|   }
 | |
|   
 | |
|   console.log('DSN return types test completed successfully');
 | |
| });
 | |
| 
 | |
| tap.test('CEP-10: MDN (Message Disposition Notification)', async () => {
 | |
|   const smtpClient = await createSmtpClient({
 | |
|     host: testServer.hostname,
 | |
|     port: testServer.port,
 | |
|     secure: false,
 | |
|     connectionTimeout: 5000
 | |
|   });
 | |
| 
 | |
|   // 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.success).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'
 | |
|     }]
 | |
|   });
 | |
| 
 | |
|   const mdnResult = await smtpClient.sendMail(mdnResponse);
 | |
|   expect(mdnResult.success).toBeTruthy();
 | |
|   console.log('MDN test completed successfully');
 | |
| });
 | |
| 
 | |
| tap.test('CEP-10: Multiple recipients with different DSN', async () => {
 | |
|   const smtpClient = await createSmtpClient({
 | |
|     host: testServer.hostname,
 | |
|     port: testServer.port,
 | |
|     secure: false,
 | |
|     connectionTimeout: 5000
 | |
|   });
 | |
| 
 | |
|   // Email with multiple recipients
 | |
|   const emails = [
 | |
|     {
 | |
|       to: 'important@example.com',
 | |
|       dsn: 'SUCCESS,FAILURE,DELAY'
 | |
|     },
 | |
|     {
 | |
|       to: 'normal@example.com', 
 | |
|       dsn: 'FAILURE'
 | |
|     },
 | |
|     {
 | |
|       to: 'optional@example.com',
 | |
|       dsn: 'NEVER'
 | |
|     }
 | |
|   ];
 | |
| 
 | |
|   for (const emailData of emails) {
 | |
|     const email = new Email({
 | |
|       from: 'sender@example.com',
 | |
|       to: emailData.to,
 | |
|       subject: 'Multi-recipient DSN Test',
 | |
|       text: 'Testing per-recipient DSN options',
 | |
|       headers: {
 | |
|         'X-DSN-Notify': emailData.dsn,
 | |
|         'X-DSN-Return': 'HEADERS'
 | |
|       }
 | |
|     });
 | |
| 
 | |
|     const result = await smtpClient.sendMail(email);
 | |
|     expect(result.success).toBeTruthy();
 | |
|   }
 | |
|   
 | |
|   console.log('Multiple recipients DSN test completed successfully');
 | |
| });
 | |
| 
 | |
| tap.test('CEP-10: DSN with ORCPT', async () => {
 | |
|   const smtpClient = await createSmtpClient({
 | |
|     host: testServer.hostname,
 | |
|     port: testServer.port,
 | |
|     secure: false,
 | |
|     connectionTimeout: 5000
 | |
|   });
 | |
| 
 | |
|   // 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',
 | |
|     headers: {
 | |
|       'X-DSN-Notify': 'SUCCESS,FAILURE',
 | |
|       'X-DSN-Return': 'HEADERS',
 | |
|       'X-Original-Recipient': 'rfc822;original@example.com'
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   const result = await smtpClient.sendMail(email);
 | |
|   expect(result.success).toBeTruthy();
 | |
|   console.log('DSN with ORCPT test sent successfully');
 | |
| });
 | |
| 
 | |
| tap.test('CEP-10: Receipt request formats', async () => {
 | |
|   const smtpClient = await createSmtpClient({
 | |
|     host: testServer.hostname,
 | |
|     port: testServer.port,
 | |
|     secure: false,
 | |
|     connectionTimeout: 5000
 | |
|   });
 | |
| 
 | |
|   // 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(`Testing 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
 | |
|       }
 | |
|     });
 | |
| 
 | |
|     const result = await smtpClient.sendMail(email);
 | |
|     expect(result.success).toBeTruthy();
 | |
|   }
 | |
|   
 | |
|   console.log('Receipt request formats test completed successfully');
 | |
| });
 | |
| 
 | |
| tap.test('CEP-10: Non-delivery reports', async () => {
 | |
|   const smtpClient = await createSmtpClient({
 | |
|     host: testServer.hostname,
 | |
|     port: testServer.port,
 | |
|     secure: false,
 | |
|     connectionTimeout: 5000
 | |
|   });
 | |
| 
 | |
|   // 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'
 | |
|       }
 | |
|     ]
 | |
|   });
 | |
| 
 | |
|   const result = await smtpClient.sendMail(ndrEmail);
 | |
|   expect(result.success).toBeTruthy();
 | |
|   console.log('Non-delivery report test sent successfully');
 | |
| });
 | |
| 
 | |
| tap.test('CEP-10: Delivery delay notifications', async () => {
 | |
|   const smtpClient = await createSmtpClient({
 | |
|     host: testServer.hostname,
 | |
|     port: testServer.port,
 | |
|     secure: false,
 | |
|     connectionTimeout: 5000
 | |
|   });
 | |
| 
 | |
|   // 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'
 | |
|     }]
 | |
|   });
 | |
| 
 | |
|   const result = await smtpClient.sendMail(delayNotification);
 | |
|   expect(result.success).toBeTruthy();
 | |
|   console.log('Delivery delay notification test sent successfully');
 | |
| });
 | |
| 
 | |
| tap.test('cleanup test SMTP server', async () => {
 | |
|   if (testServer) {
 | |
|     await stopTestServer(testServer);
 | |
|   }
 | |
| });
 | |
| 
 | |
| export default tap.start(); |