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
		
			
				
	
	
		
			408 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			408 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import { tap, expect } from '@git.zone/tstest/tapbundle';
 | 
						|
import * as plugins from '../../../ts/plugins.ts';
 | 
						|
import * as net from 'net';
 | 
						|
import { startTestServer, stopTestServer } from '../../helpers/server.loader.ts'
 | 
						|
import type { ITestServer } from '../../helpers/server.loader.ts';
 | 
						|
 | 
						|
const TEST_PORT = 2525;
 | 
						|
let testServer: ITestServer;
 | 
						|
 | 
						|
// Helper function to wait for SMTP response
 | 
						|
const waitForResponse = (socket: net.Socket, expectedCode?: string, timeout = 5000): Promise<string> => {
 | 
						|
  return new Promise((resolve, reject) => {
 | 
						|
    let buffer = '';
 | 
						|
    const timer = setTimeout(() => {
 | 
						|
      socket.removeListener('data', handler);
 | 
						|
      reject(new Error(`Timeout waiting for ${expectedCode || 'any'} response`));
 | 
						|
    }, timeout);
 | 
						|
    
 | 
						|
    const handler = (data: Buffer) => {
 | 
						|
      buffer += data.toString();
 | 
						|
      const lines = buffer.split('\r\n');
 | 
						|
      
 | 
						|
      // Check if we have a complete response
 | 
						|
      for (const line of lines) {
 | 
						|
        if (expectedCode) {
 | 
						|
          if (line.startsWith(expectedCode + ' ')) {
 | 
						|
            clearTimeout(timer);
 | 
						|
            socket.removeListener('data', handler);
 | 
						|
            resolve(buffer);
 | 
						|
            return;
 | 
						|
          }
 | 
						|
        } else {
 | 
						|
          // Any complete response line
 | 
						|
          if (line.match(/^\d{3} /)) {
 | 
						|
            clearTimeout(timer);
 | 
						|
            socket.removeListener('data', handler);
 | 
						|
            resolve(buffer);
 | 
						|
            return;
 | 
						|
          }
 | 
						|
        }
 | 
						|
      }
 | 
						|
    };
 | 
						|
    
 | 
						|
    socket.on('data', handler);
 | 
						|
  });
 | 
						|
};
 | 
						|
 | 
						|
tap.test('setup - start test server', async (toolsArg) => {
 | 
						|
  testServer = await startTestServer({ port: TEST_PORT });
 | 
						|
  await toolsArg.delayFor(1000);
 | 
						|
});
 | 
						|
 | 
						|
tap.test('RFC 7489 DMARC - Server handles DMARC policies', async (tools) => {
 | 
						|
  const done = tools.defer();
 | 
						|
  
 | 
						|
  const socket = net.createConnection({
 | 
						|
    host: 'localhost',
 | 
						|
    port: TEST_PORT,
 | 
						|
    timeout: 30000
 | 
						|
  });
 | 
						|
  
 | 
						|
  socket.on('error', (err) => {
 | 
						|
    console.error('Socket error:', err);
 | 
						|
    done.reject(err);
 | 
						|
  });
 | 
						|
  
 | 
						|
  socket.on('connect', async () => {
 | 
						|
    try {
 | 
						|
      const dmarcResults: any[] = [];
 | 
						|
      
 | 
						|
      // Test domains simulating different DMARC policies
 | 
						|
      const dmarcTestScenarios = [
 | 
						|
        {
 | 
						|
          domain: 'dmarc-reject.example.com',
 | 
						|
          policy: 'reject',
 | 
						|
          alignment: 'strict'
 | 
						|
        },
 | 
						|
        {
 | 
						|
          domain: 'dmarc-quarantine.example.com', 
 | 
						|
          policy: 'quarantine',
 | 
						|
          alignment: 'relaxed'
 | 
						|
        },
 | 
						|
        {
 | 
						|
          domain: 'dmarc-none.example.com',
 | 
						|
          policy: 'none',
 | 
						|
          alignment: 'relaxed'
 | 
						|
        }
 | 
						|
      ];
 | 
						|
      
 | 
						|
      // Wait for greeting
 | 
						|
      await waitForResponse(socket, '220');
 | 
						|
      
 | 
						|
      // Send EHLO
 | 
						|
      socket.write('EHLO testclient\r\n');
 | 
						|
      const ehloResponse = await waitForResponse(socket, '250');
 | 
						|
      
 | 
						|
      // Check if server advertises DMARC support
 | 
						|
      const advertisesDmarc = ehloResponse.toLowerCase().includes('dmarc');
 | 
						|
      console.log('Server advertises DMARC:', advertisesDmarc);
 | 
						|
      
 | 
						|
      // Test each scenario
 | 
						|
      for (let i = 0; i < dmarcTestScenarios.length; i++) {
 | 
						|
        const scenario = dmarcTestScenarios[i];
 | 
						|
        const testFromAddress = `dmarc-test@${scenario.domain}`;
 | 
						|
        
 | 
						|
        dmarcResults[i] = {
 | 
						|
          domain: scenario.domain,
 | 
						|
          policy: scenario.policy,
 | 
						|
          mailFromAccepted: false,
 | 
						|
          rcptAccepted: false
 | 
						|
        };
 | 
						|
        
 | 
						|
        console.log(`Testing DMARC policy: ${scenario.policy} for domain: ${scenario.domain}`);
 | 
						|
        socket.write(`MAIL FROM:<${testFromAddress}>\r\n`);
 | 
						|
        const mailResponse = await waitForResponse(socket);
 | 
						|
        
 | 
						|
        dmarcResults[i].mailFromResponse = mailResponse.trim();
 | 
						|
        
 | 
						|
        if (mailResponse.includes('250')) {
 | 
						|
          dmarcResults[i].mailFromAccepted = true;
 | 
						|
          
 | 
						|
          socket.write(`RCPT TO:<recipient@example.com>\r\n`);
 | 
						|
          const rcptResponse = await waitForResponse(socket);
 | 
						|
          
 | 
						|
          if (rcptResponse.includes('250')) {
 | 
						|
            dmarcResults[i].rcptAccepted = true;
 | 
						|
            
 | 
						|
            // Send DATA
 | 
						|
            socket.write('DATA\r\n');
 | 
						|
            await waitForResponse(socket, '354');
 | 
						|
            
 | 
						|
            // Send email with DMARC-relevant headers
 | 
						|
            const email = [
 | 
						|
              `From: dmarc-test@${scenario.domain}`,
 | 
						|
              `To: recipient@example.com`,
 | 
						|
              `Subject: DMARC RFC 7489 Compliance Test - ${scenario.policy}`,
 | 
						|
              `Date: ${new Date().toUTCString()}`,
 | 
						|
              `Message-ID: <dmarc-test-${scenario.policy}-${Date.now()}@${scenario.domain}>`,
 | 
						|
              `DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=${scenario.domain}; s=default;`,
 | 
						|
              `        h=from:to:subject:date; bh=testbodyhash; b=testsignature`,
 | 
						|
              `Authentication-Results: example.org; spf=pass smtp.mailfrom=${scenario.domain}`,
 | 
						|
              '',
 | 
						|
              `This email tests DMARC ${scenario.policy} policy compliance.`,
 | 
						|
              'The server should handle DMARC policies according to RFC 7489.',
 | 
						|
              '.',
 | 
						|
              ''
 | 
						|
            ].join('\r\n');
 | 
						|
            
 | 
						|
            socket.write(email);
 | 
						|
            const dataResponse = await waitForResponse(socket, '250');
 | 
						|
            
 | 
						|
            dmarcResults[i].emailAccepted = true;
 | 
						|
            console.log(`DMARC ${scenario.policy} policy email accepted`);
 | 
						|
          }
 | 
						|
        } else if (mailResponse.includes('550') || mailResponse.includes('553')) {
 | 
						|
          // DMARC policy rejection (expected for some scenarios)
 | 
						|
          dmarcResults[i].dmarcRejected = true;
 | 
						|
          dmarcResults[i].rejectionResponse = mailResponse.trim();
 | 
						|
          console.log(`DMARC ${scenario.policy} policy rejected as expected`);
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Reset for next test
 | 
						|
        socket.write('RSET\r\n');
 | 
						|
        await waitForResponse(socket, '250');
 | 
						|
      }
 | 
						|
      
 | 
						|
      // All tests complete
 | 
						|
      console.log('DMARC test results:', dmarcResults);
 | 
						|
      
 | 
						|
      // Check that server handled all scenarios
 | 
						|
      const allScenariosHandled = dmarcResults.every(result => 
 | 
						|
        result.mailFromResponse !== undefined
 | 
						|
      );
 | 
						|
      
 | 
						|
      expect(allScenariosHandled).toEqual(true);
 | 
						|
      
 | 
						|
      // Send QUIT
 | 
						|
      socket.write('QUIT\r\n');
 | 
						|
      await waitForResponse(socket, '221');
 | 
						|
      
 | 
						|
      socket.end();
 | 
						|
      done.resolve();
 | 
						|
    } catch (err) {
 | 
						|
      console.error('Test error:', err);
 | 
						|
      socket.end();
 | 
						|
      done.reject(err);
 | 
						|
    }
 | 
						|
  });
 | 
						|
  
 | 
						|
  await done.promise;
 | 
						|
});
 | 
						|
 | 
						|
tap.test('RFC 7489 DMARC - Alignment testing', async (tools) => {
 | 
						|
  const done = tools.defer();
 | 
						|
  
 | 
						|
  const socket = net.createConnection({
 | 
						|
    host: 'localhost',
 | 
						|
    port: TEST_PORT,
 | 
						|
    timeout: 30000
 | 
						|
  });
 | 
						|
  
 | 
						|
  socket.on('error', (err) => {
 | 
						|
    console.error('Socket error:', err);
 | 
						|
    done.reject(err);
 | 
						|
  });
 | 
						|
  
 | 
						|
  socket.on('connect', async () => {
 | 
						|
    try {
 | 
						|
      // Wait for greeting
 | 
						|
      await waitForResponse(socket, '220');
 | 
						|
      
 | 
						|
      // Send EHLO
 | 
						|
      socket.write('EHLO testclient\r\n');
 | 
						|
      await waitForResponse(socket, '250');
 | 
						|
      
 | 
						|
      // Test misaligned domain (envelope vs header)
 | 
						|
      socket.write('MAIL FROM:<sender@envelope-domain.com>\r\n');
 | 
						|
      await waitForResponse(socket, '250');
 | 
						|
      
 | 
						|
      socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
						|
      await waitForResponse(socket, '250');
 | 
						|
      
 | 
						|
      socket.write('DATA\r\n');
 | 
						|
      await waitForResponse(socket, '354');
 | 
						|
      
 | 
						|
      // Email with different header From domain (testing alignment)
 | 
						|
      const email = [
 | 
						|
        `From: sender@header-domain.com`,
 | 
						|
        `To: recipient@example.com`,
 | 
						|
        `Subject: DMARC Alignment Test`,
 | 
						|
        `Date: ${new Date().toUTCString()}`,
 | 
						|
        `Message-ID: <alignment-${Date.now()}@header-domain.com>`,
 | 
						|
        `DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=header-domain.com; s=default;`,
 | 
						|
        `        h=from:to:subject:date; bh=alignmenthash; b=alignmentsig`,
 | 
						|
        '',
 | 
						|
        'Testing DMARC domain alignment (envelope vs header From).',
 | 
						|
        '.',
 | 
						|
        ''
 | 
						|
      ].join('\r\n');
 | 
						|
      
 | 
						|
      socket.write(email);
 | 
						|
      const response = await waitForResponse(socket);
 | 
						|
      
 | 
						|
      const accepted = response.includes('250');
 | 
						|
      console.log(`Alignment test ${accepted ? 'accepted' : 'rejected due to alignment failure'}`);
 | 
						|
      
 | 
						|
      // Send QUIT
 | 
						|
      socket.write('QUIT\r\n');
 | 
						|
      await waitForResponse(socket, '221');
 | 
						|
      
 | 
						|
      socket.end();
 | 
						|
      done.resolve();
 | 
						|
    } catch (err) {
 | 
						|
      console.error('Test error:', err);
 | 
						|
      socket.end();
 | 
						|
      done.reject(err);
 | 
						|
    }
 | 
						|
  });
 | 
						|
  
 | 
						|
  await done.promise;
 | 
						|
});
 | 
						|
 | 
						|
tap.test('RFC 7489 DMARC - Subdomain policy', async (tools) => {
 | 
						|
  const done = tools.defer();
 | 
						|
  
 | 
						|
  const socket = net.createConnection({
 | 
						|
    host: 'localhost',
 | 
						|
    port: TEST_PORT,
 | 
						|
    timeout: 30000
 | 
						|
  });
 | 
						|
  
 | 
						|
  socket.on('error', (err) => {
 | 
						|
    console.error('Socket error:', err);
 | 
						|
    done.reject(err);
 | 
						|
  });
 | 
						|
  
 | 
						|
  socket.on('connect', async () => {
 | 
						|
    try {
 | 
						|
      // Wait for greeting
 | 
						|
      await waitForResponse(socket, '220');
 | 
						|
      
 | 
						|
      // Send EHLO
 | 
						|
      socket.write('EHLO testclient\r\n');
 | 
						|
      await waitForResponse(socket, '250');
 | 
						|
      
 | 
						|
      // Test subdomain policy inheritance
 | 
						|
      socket.write('MAIL FROM:<sender@subdomain.dmarc-policy.com>\r\n');
 | 
						|
      await waitForResponse(socket, '250');
 | 
						|
      
 | 
						|
      socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
						|
      await waitForResponse(socket, '250');
 | 
						|
      
 | 
						|
      socket.write('DATA\r\n');
 | 
						|
      await waitForResponse(socket, '354');
 | 
						|
      
 | 
						|
      // Email from subdomain to test policy inheritance
 | 
						|
      const email = [
 | 
						|
        `From: sender@subdomain.dmarc-policy.com`,
 | 
						|
        `To: recipient@example.com`,
 | 
						|
        `Subject: DMARC Subdomain Policy Test`,
 | 
						|
        `Date: ${new Date().toUTCString()}`,
 | 
						|
        `Message-ID: <subdomain-${Date.now()}@subdomain.dmarc-policy.com>`,
 | 
						|
        `DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=subdomain.dmarc-policy.com; s=default;`,
 | 
						|
        `        h=from:to:subject:date; bh=subdomainhash; b=subdomainsig`,
 | 
						|
        '',
 | 
						|
        'Testing DMARC subdomain policy inheritance.',
 | 
						|
        '.',
 | 
						|
        ''
 | 
						|
      ].join('\r\n');
 | 
						|
      
 | 
						|
      socket.write(email);
 | 
						|
      const response = await waitForResponse(socket);
 | 
						|
      
 | 
						|
      const accepted = response.includes('250');
 | 
						|
      console.log(`Subdomain policy test ${accepted ? 'accepted' : 'rejected'}`);
 | 
						|
      
 | 
						|
      // Send QUIT
 | 
						|
      socket.write('QUIT\r\n');
 | 
						|
      await waitForResponse(socket, '221');
 | 
						|
      
 | 
						|
      socket.end();
 | 
						|
      done.resolve();
 | 
						|
    } catch (err) {
 | 
						|
      console.error('Test error:', err);
 | 
						|
      socket.end();
 | 
						|
      done.reject(err);
 | 
						|
    }
 | 
						|
  });
 | 
						|
  
 | 
						|
  await done.promise;
 | 
						|
});
 | 
						|
 | 
						|
tap.test('RFC 7489 DMARC - Report generation hint', async (tools) => {
 | 
						|
  const done = tools.defer();
 | 
						|
  
 | 
						|
  const socket = net.createConnection({
 | 
						|
    host: 'localhost',
 | 
						|
    port: TEST_PORT,
 | 
						|
    timeout: 30000
 | 
						|
  });
 | 
						|
  
 | 
						|
  socket.on('error', (err) => {
 | 
						|
    console.error('Socket error:', err);
 | 
						|
    done.reject(err);
 | 
						|
  });
 | 
						|
  
 | 
						|
  socket.on('connect', async () => {
 | 
						|
    try {
 | 
						|
      // Wait for greeting
 | 
						|
      await waitForResponse(socket, '220');
 | 
						|
      
 | 
						|
      // Send EHLO
 | 
						|
      socket.write('EHLO testclient\r\n');
 | 
						|
      await waitForResponse(socket, '250');
 | 
						|
      
 | 
						|
      socket.write('MAIL FROM:<dmarc-report@example.com>\r\n');
 | 
						|
      await waitForResponse(socket, '250');
 | 
						|
      
 | 
						|
      socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
						|
      await waitForResponse(socket, '250');
 | 
						|
      
 | 
						|
      socket.write('DATA\r\n');
 | 
						|
      await waitForResponse(socket, '354');
 | 
						|
      
 | 
						|
      // Email with DMARC report request headers
 | 
						|
      const email = [
 | 
						|
        `From: dmarc-report@example.com`,
 | 
						|
        `To: recipient@example.com`,
 | 
						|
        `Subject: DMARC Report Generation Test`,
 | 
						|
        `Date: ${new Date().toUTCString()}`,
 | 
						|
        `Message-ID: <report-${Date.now()}@example.com>`,
 | 
						|
        `DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=default;`,
 | 
						|
        `        h=from:to:subject:date; bh=reporthash; b=reportsig`,
 | 
						|
        `Authentication-Results: mta.example.com;`,
 | 
						|
        `        dmarc=pass (p=none dis=none) header.from=example.com`,
 | 
						|
        '',
 | 
						|
        'Testing DMARC report generation capabilities.',
 | 
						|
        'Server should log DMARC results for reporting.',
 | 
						|
        '.',
 | 
						|
        ''
 | 
						|
      ].join('\r\n');
 | 
						|
      
 | 
						|
      socket.write(email);
 | 
						|
      await waitForResponse(socket, '250');
 | 
						|
      
 | 
						|
      console.log('DMARC report test email accepted');
 | 
						|
      
 | 
						|
      // Send QUIT
 | 
						|
      socket.write('QUIT\r\n');
 | 
						|
      await waitForResponse(socket, '221');
 | 
						|
      
 | 
						|
      socket.end();
 | 
						|
      done.resolve();
 | 
						|
    } catch (err) {
 | 
						|
      console.error('Test error:', err);
 | 
						|
      socket.end();
 | 
						|
      done.reject(err);
 | 
						|
    }
 | 
						|
  });
 | 
						|
  
 | 
						|
  await done.promise;
 | 
						|
});
 | 
						|
 | 
						|
tap.test('cleanup - stop test server', async () => {
 | 
						|
  await stopTestServer(testServer);
 | 
						|
});
 | 
						|
 | 
						|
export default tap.start(); |