feat(storage): add comprehensive tests for StorageManager with memory, filesystem, and custom function backends
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
This commit is contained in:
		@@ -0,0 +1,315 @@
 | 
			
		||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
 | 
			
		||||
import * as net from 'net';
 | 
			
		||||
import { startTestServer, stopTestServer } from '../../helpers/server.loader.ts'
 | 
			
		||||
import type { ITestServer } from '../../helpers/server.loader.ts';
 | 
			
		||||
// Test configuration
 | 
			
		||||
const TEST_PORT = 2525;
 | 
			
		||||
const TEST_TIMEOUT = 20000;
 | 
			
		||||
 | 
			
		||||
let testServer: ITestServer;
 | 
			
		||||
 | 
			
		||||
// Setup
 | 
			
		||||
tap.test('setup - start SMTP server', async () => {
 | 
			
		||||
  testServer = await startTestServer({ port: TEST_PORT });
 | 
			
		||||
  await new Promise(resolve => setTimeout(resolve, 1000));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Test: Invalid email address validation
 | 
			
		||||
tap.test('Invalid Email Addresses - should reject various invalid email formats', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const invalidAddresses = [
 | 
			
		||||
    'invalid-email',
 | 
			
		||||
    '@example.com',
 | 
			
		||||
    'user@',
 | 
			
		||||
    'user..name@example.com',
 | 
			
		||||
    'user@.example.com',
 | 
			
		||||
    'user@example..com',
 | 
			
		||||
    'user@example.',
 | 
			
		||||
    'user name@example.com',
 | 
			
		||||
    'user@exam ple.com',
 | 
			
		||||
    'user@[invalid]',
 | 
			
		||||
    'a'.repeat(65) + '@example.com', // Local part too long
 | 
			
		||||
    'user@' + 'a'.repeat(250) + '.com' // Domain too long
 | 
			
		||||
  ];
 | 
			
		||||
  
 | 
			
		||||
  const results: Array<{
 | 
			
		||||
    address: string;
 | 
			
		||||
    response: string;
 | 
			
		||||
    responseCode: string;
 | 
			
		||||
    properlyRejected: boolean;
 | 
			
		||||
    accepted: boolean;
 | 
			
		||||
  }> = [];
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: TEST_TIMEOUT
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let currentIndex = 0;
 | 
			
		||||
  let state = 'connecting';
 | 
			
		||||
  let buffer = '';
 | 
			
		||||
  let lastResponseCode = '';
 | 
			
		||||
  const fromAddress = 'test@example.com';
 | 
			
		||||
  
 | 
			
		||||
  const processNextAddress = () => {
 | 
			
		||||
    if (currentIndex < invalidAddresses.length) {
 | 
			
		||||
      socket.write(`RCPT TO:<${invalidAddresses[currentIndex]}>\r\n`);
 | 
			
		||||
      state = 'rcpt';
 | 
			
		||||
    } else {
 | 
			
		||||
      socket.write('QUIT\r\n');
 | 
			
		||||
      state = 'quit';
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  
 | 
			
		||||
  socket.on('data', (data) => {
 | 
			
		||||
    buffer += data.toString();
 | 
			
		||||
    const lines = buffer.split('\r\n');
 | 
			
		||||
    
 | 
			
		||||
    // Process complete lines
 | 
			
		||||
    for (let i = 0; i < lines.length - 1; i++) {
 | 
			
		||||
      const line = lines[i];
 | 
			
		||||
      if (line.match(/^\d{3}/)) {
 | 
			
		||||
        lastResponseCode = line.substring(0, 3);
 | 
			
		||||
        
 | 
			
		||||
        if (state === 'connecting' && line.startsWith('220')) {
 | 
			
		||||
          socket.write('EHLO test.example.com\r\n');
 | 
			
		||||
          state = 'ehlo';
 | 
			
		||||
        } else if (state === 'ehlo' && line.startsWith('250') && !line.includes('250-')) {
 | 
			
		||||
          socket.write(`MAIL FROM:<${fromAddress}>\r\n`);
 | 
			
		||||
          state = 'mail';
 | 
			
		||||
        } else if (state === 'mail' && line.startsWith('250')) {
 | 
			
		||||
          processNextAddress();
 | 
			
		||||
        } else if (state === 'rcpt') {
 | 
			
		||||
          // Record result
 | 
			
		||||
          const rejected = lastResponseCode.startsWith('5') || lastResponseCode.startsWith('4');
 | 
			
		||||
          results.push({
 | 
			
		||||
            address: invalidAddresses[currentIndex],
 | 
			
		||||
            response: line,
 | 
			
		||||
            responseCode: lastResponseCode,
 | 
			
		||||
            properlyRejected: rejected,
 | 
			
		||||
            accepted: lastResponseCode.startsWith('2')
 | 
			
		||||
          });
 | 
			
		||||
          
 | 
			
		||||
          currentIndex++;
 | 
			
		||||
          
 | 
			
		||||
          if (currentIndex < invalidAddresses.length) {
 | 
			
		||||
            // Reset and test next
 | 
			
		||||
            socket.write('RSET\r\n');
 | 
			
		||||
            state = 'rset';
 | 
			
		||||
          } else {
 | 
			
		||||
            socket.write('QUIT\r\n');
 | 
			
		||||
            state = 'quit';
 | 
			
		||||
          }
 | 
			
		||||
        } else if (state === 'rset' && line.startsWith('250')) {
 | 
			
		||||
          socket.write(`MAIL FROM:<${fromAddress}>\r\n`);
 | 
			
		||||
          state = 'mail';
 | 
			
		||||
        } else if (state === 'quit' && line.startsWith('221')) {
 | 
			
		||||
          socket.destroy();
 | 
			
		||||
          
 | 
			
		||||
          // Analyze results
 | 
			
		||||
          const rejected = results.filter(r => r.properlyRejected).length;
 | 
			
		||||
          const rate = results.length > 0 ? rejected / results.length : 0;
 | 
			
		||||
          
 | 
			
		||||
          // Log results for debugging
 | 
			
		||||
          results.forEach(r => {
 | 
			
		||||
            if (!r.properlyRejected) {
 | 
			
		||||
              console.log(`WARNING: Invalid address accepted: ${r.address}`);
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
          
 | 
			
		||||
          // We expect at least 70% rejection rate for invalid addresses
 | 
			
		||||
          expect(rate).toBeGreaterThan(0.7);
 | 
			
		||||
          expect(results.length).toEqual(invalidAddresses.length);
 | 
			
		||||
          
 | 
			
		||||
          done.resolve();
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Keep incomplete line in buffer
 | 
			
		||||
    buffer = lines[lines.length - 1];
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('timeout', () => {
 | 
			
		||||
    socket.destroy();
 | 
			
		||||
    done.reject(new Error('Test timeout'));
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('error', (err) => {
 | 
			
		||||
    done.reject(err);
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  await done.promise;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Test: Edge case email addresses that might be valid
 | 
			
		||||
tap.test('Invalid Email Addresses - should handle edge case addresses', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const edgeCaseAddresses = [
 | 
			
		||||
    'user+tag@example.com', // Valid - with plus addressing
 | 
			
		||||
    'user.name@example.com', // Valid - with dot
 | 
			
		||||
    'user@sub.example.com', // Valid - subdomain
 | 
			
		||||
    'user@192.168.1.1', // Valid - IP address
 | 
			
		||||
    'user@[192.168.1.1]', // Valid - IP in brackets
 | 
			
		||||
    '"user name"@example.com', // Valid - quoted local part
 | 
			
		||||
    'user\\@name@example.com', // Valid - escaped character
 | 
			
		||||
    'user@localhost', // Might be valid depending on server config
 | 
			
		||||
  ];
 | 
			
		||||
  
 | 
			
		||||
  const results: Array<{
 | 
			
		||||
    address: string;
 | 
			
		||||
    accepted: boolean;
 | 
			
		||||
  }> = [];
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: TEST_TIMEOUT
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let currentIndex = 0;
 | 
			
		||||
  let state = 'connecting';
 | 
			
		||||
  let buffer = '';
 | 
			
		||||
  const fromAddress = 'test@example.com';
 | 
			
		||||
  
 | 
			
		||||
  socket.on('data', (data) => {
 | 
			
		||||
    buffer += data.toString();
 | 
			
		||||
    const lines = buffer.split('\r\n');
 | 
			
		||||
    
 | 
			
		||||
    for (let i = 0; i < lines.length - 1; i++) {
 | 
			
		||||
      const line = lines[i];
 | 
			
		||||
      if (line.match(/^\d{3}/)) {
 | 
			
		||||
        const responseCode = line.substring(0, 3);
 | 
			
		||||
        
 | 
			
		||||
        if (state === 'connecting' && line.startsWith('220')) {
 | 
			
		||||
          socket.write('EHLO test.example.com\r\n');
 | 
			
		||||
          state = 'ehlo';
 | 
			
		||||
        } else if (state === 'ehlo' && line.startsWith('250') && !line.includes('250-')) {
 | 
			
		||||
          socket.write(`MAIL FROM:<${fromAddress}>\r\n`);
 | 
			
		||||
          state = 'mail';
 | 
			
		||||
        } else if (state === 'mail' && line.startsWith('250')) {
 | 
			
		||||
          if (currentIndex < edgeCaseAddresses.length) {
 | 
			
		||||
            socket.write(`RCPT TO:<${edgeCaseAddresses[currentIndex]}>\r\n`);
 | 
			
		||||
            state = 'rcpt';
 | 
			
		||||
          } else {
 | 
			
		||||
            socket.write('QUIT\r\n');
 | 
			
		||||
            state = 'quit';
 | 
			
		||||
          }
 | 
			
		||||
        } else if (state === 'rcpt') {
 | 
			
		||||
          results.push({
 | 
			
		||||
            address: edgeCaseAddresses[currentIndex],
 | 
			
		||||
            accepted: responseCode.startsWith('2')
 | 
			
		||||
          });
 | 
			
		||||
          
 | 
			
		||||
          currentIndex++;
 | 
			
		||||
          
 | 
			
		||||
          if (currentIndex < edgeCaseAddresses.length) {
 | 
			
		||||
            socket.write('RSET\r\n');
 | 
			
		||||
            state = 'rset';
 | 
			
		||||
          } else {
 | 
			
		||||
            socket.write('QUIT\r\n');
 | 
			
		||||
            state = 'quit';
 | 
			
		||||
          }
 | 
			
		||||
        } else if (state === 'rset' && line.startsWith('250')) {
 | 
			
		||||
          socket.write(`MAIL FROM:<${fromAddress}>\r\n`);
 | 
			
		||||
          state = 'mail';
 | 
			
		||||
        } else if (state === 'quit' && line.startsWith('221')) {
 | 
			
		||||
          socket.destroy();
 | 
			
		||||
          
 | 
			
		||||
          // Just verify we tested all addresses
 | 
			
		||||
          expect(results.length).toEqual(edgeCaseAddresses.length);
 | 
			
		||||
          
 | 
			
		||||
          done.resolve();
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    buffer = lines[lines.length - 1];
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('timeout', () => {
 | 
			
		||||
    socket.destroy();
 | 
			
		||||
    done.reject(new Error('Test timeout'));
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('error', (err) => {
 | 
			
		||||
    done.reject(err);
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  await done.promise;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Test: Empty and null addresses
 | 
			
		||||
tap.test('Invalid Email Addresses - should handle empty addresses', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: TEST_TIMEOUT
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let receivedData = '';
 | 
			
		||||
  let currentStep = 'connecting';
 | 
			
		||||
  
 | 
			
		||||
  socket.on('data', (data) => {
 | 
			
		||||
    receivedData += data.toString();
 | 
			
		||||
    
 | 
			
		||||
    if (currentStep === 'connecting' && receivedData.includes('220')) {
 | 
			
		||||
      currentStep = 'ehlo';
 | 
			
		||||
      socket.write('EHLO test.example.com\r\n');
 | 
			
		||||
    } else if (currentStep === 'ehlo' && receivedData.includes('250')) {
 | 
			
		||||
      currentStep = 'mail_from';
 | 
			
		||||
      socket.write('MAIL FROM:<test@example.com>\r\n');
 | 
			
		||||
    } else if (currentStep === 'mail_from' && receivedData.includes('250')) {
 | 
			
		||||
      currentStep = 'rcpt_empty';
 | 
			
		||||
      socket.write('RCPT TO:<>\r\n'); // Empty address
 | 
			
		||||
    } else if (currentStep === 'rcpt_empty') {
 | 
			
		||||
      if (receivedData.includes('250')) {
 | 
			
		||||
        // Empty recipient allowed (for bounces)
 | 
			
		||||
        currentStep = 'rset';
 | 
			
		||||
        socket.write('RSET\r\n');
 | 
			
		||||
      } else if (receivedData.match(/[45]\d{2}/)) {
 | 
			
		||||
        // Empty recipient rejected
 | 
			
		||||
        currentStep = 'rset';
 | 
			
		||||
        socket.write('RSET\r\n');
 | 
			
		||||
      }
 | 
			
		||||
    } else if (currentStep === 'rset' && receivedData.includes('250')) {
 | 
			
		||||
      currentStep = 'mail_empty';
 | 
			
		||||
      socket.write('MAIL FROM:<>\r\n'); // Empty sender (bounce)
 | 
			
		||||
    } else if (currentStep === 'mail_empty' && receivedData.includes('250')) {
 | 
			
		||||
      currentStep = 'rcpt_after_empty';
 | 
			
		||||
      socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
			
		||||
    } else if (currentStep === 'rcpt_after_empty' && receivedData.includes('250')) {
 | 
			
		||||
      socket.write('QUIT\r\n');
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        socket.destroy();
 | 
			
		||||
        // Empty MAIL FROM should be accepted for bounces
 | 
			
		||||
        expect(receivedData).toInclude('250');
 | 
			
		||||
        done.resolve();
 | 
			
		||||
      }, 100);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('error', (error) => {
 | 
			
		||||
    done.reject(error);
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('timeout', () => {
 | 
			
		||||
    socket.destroy();
 | 
			
		||||
    done.reject(new Error(`Connection timeout at step: ${currentStep}`));
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  await done.promise;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Teardown
 | 
			
		||||
tap.test('teardown - stop SMTP server', async () => {
 | 
			
		||||
  await stopTestServer(testServer);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Start the test
 | 
			
		||||
export default tap.start();
 | 
			
		||||
		Reference in New Issue
	
	Block a user