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,450 @@
 | 
			
		||||
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 6376 DKIM - Server accepts email with DKIM signature', 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');
 | 
			
		||||
      
 | 
			
		||||
      // Send MAIL FROM
 | 
			
		||||
      socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
      await waitForResponse(socket, '250');
 | 
			
		||||
      
 | 
			
		||||
      // Send RCPT TO
 | 
			
		||||
      socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
			
		||||
      await waitForResponse(socket, '250');
 | 
			
		||||
      
 | 
			
		||||
      // Send DATA
 | 
			
		||||
      socket.write('DATA\r\n');
 | 
			
		||||
      await waitForResponse(socket, '354');
 | 
			
		||||
      
 | 
			
		||||
      // Create email with DKIM signature
 | 
			
		||||
      const dkimSignature = [
 | 
			
		||||
        'DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;',
 | 
			
		||||
        '        d=example.com; s=default;',
 | 
			
		||||
        '        h=from:to:subject:date:message-id;',
 | 
			
		||||
        '        bh=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN/XKdLCPjaYaY=;',
 | 
			
		||||
        '        b=Kt1zLCYmUVYJKEOVL9nGF2JVPJ5/k5l6yOkNBJGCrZn4E5z9Qn7TlYrG8QfBgJ4',
 | 
			
		||||
        '        CzYVLjKm5xOhUoEaDzTJ1E6C9A4hL8sKfBxQjN8oWv4kP3GdE6mFqS0wKcRjT+',
 | 
			
		||||
        '        NxOz2VcJP4LmKjFsG8XqBhYoEfCvSr3UwNmEkP6RjT9WlQzA4kJe2VoMsJ='
 | 
			
		||||
      ].join('\r\n');
 | 
			
		||||
      
 | 
			
		||||
      const email = [
 | 
			
		||||
        `From: sender@example.com`,
 | 
			
		||||
        `To: recipient@example.com`,
 | 
			
		||||
        `Subject: DKIM RFC 6376 Compliance Test`,
 | 
			
		||||
        `Date: ${new Date().toUTCString()}`,
 | 
			
		||||
        `Message-ID: <dkim-test-${Date.now()}@example.com>`,
 | 
			
		||||
        dkimSignature,
 | 
			
		||||
        '',
 | 
			
		||||
        'This email tests RFC 6376 DKIM compliance.',
 | 
			
		||||
        'The server should properly handle DKIM signatures.',
 | 
			
		||||
        '.',
 | 
			
		||||
        ''
 | 
			
		||||
      ].join('\r\n');
 | 
			
		||||
      
 | 
			
		||||
      socket.write(email);
 | 
			
		||||
      await waitForResponse(socket, '250');
 | 
			
		||||
      
 | 
			
		||||
      console.log('Email with DKIM signature accepted');
 | 
			
		||||
      expect(true).toEqual(true); // Server accepts DKIM headers
 | 
			
		||||
      
 | 
			
		||||
      // 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 6376 DKIM - Multiple DKIM signatures', 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');
 | 
			
		||||
      
 | 
			
		||||
      // Send MAIL FROM
 | 
			
		||||
      socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
      await waitForResponse(socket, '250');
 | 
			
		||||
      
 | 
			
		||||
      // Send RCPT TO
 | 
			
		||||
      socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
			
		||||
      await waitForResponse(socket, '250');
 | 
			
		||||
      
 | 
			
		||||
      // Send DATA
 | 
			
		||||
      socket.write('DATA\r\n');
 | 
			
		||||
      await waitForResponse(socket, '354');
 | 
			
		||||
      
 | 
			
		||||
      // Email with multiple DKIM signatures (common in forwarding scenarios)
 | 
			
		||||
      const email = [
 | 
			
		||||
        `From: sender@example.com`,
 | 
			
		||||
        `To: recipient@example.com`,
 | 
			
		||||
        `Subject: Multiple DKIM Signatures Test`,
 | 
			
		||||
        `Date: ${new Date().toUTCString()}`,
 | 
			
		||||
        `Message-ID: <multi-dkim-${Date.now()}@example.com>`,
 | 
			
		||||
        'DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;',
 | 
			
		||||
        '        d=example.com; s=selector1;',
 | 
			
		||||
        '        h=from:to:subject:date;',
 | 
			
		||||
        '        bh=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN/XKdLCPjaYaY=;',
 | 
			
		||||
        '        b=signature1data',
 | 
			
		||||
        'DKIM-Signature: v=1; a=rsa-sha256; c=simple/simple;',
 | 
			
		||||
        '        d=forwarder.com; s=selector2;',
 | 
			
		||||
        '        h=from:to:subject:date:message-id;',
 | 
			
		||||
        '        bh=differentbodyhash=;',
 | 
			
		||||
        '        b=signature2data',
 | 
			
		||||
        '',
 | 
			
		||||
        'Email with multiple DKIM signatures.',
 | 
			
		||||
        '.',
 | 
			
		||||
        ''
 | 
			
		||||
      ].join('\r\n');
 | 
			
		||||
      
 | 
			
		||||
      socket.write(email);
 | 
			
		||||
      await waitForResponse(socket, '250');
 | 
			
		||||
      
 | 
			
		||||
      console.log('Email with multiple DKIM signatures 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('RFC 6376 DKIM - Various canonicalization methods', 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');
 | 
			
		||||
      
 | 
			
		||||
      // Send MAIL FROM
 | 
			
		||||
      socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
      await waitForResponse(socket, '250');
 | 
			
		||||
      
 | 
			
		||||
      // Send RCPT TO
 | 
			
		||||
      socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
			
		||||
      await waitForResponse(socket, '250');
 | 
			
		||||
      
 | 
			
		||||
      // Send DATA
 | 
			
		||||
      socket.write('DATA\r\n');
 | 
			
		||||
      await waitForResponse(socket, '354');
 | 
			
		||||
      
 | 
			
		||||
      // Test different canonicalization methods
 | 
			
		||||
      const email = [
 | 
			
		||||
        `From: sender@example.com`,
 | 
			
		||||
        `To: recipient@example.com`,
 | 
			
		||||
        `Subject: DKIM Canonicalization Test`,
 | 
			
		||||
        `Date: ${new Date().toUTCString()}`,
 | 
			
		||||
        `Message-ID: <canon-${Date.now()}@example.com>`,
 | 
			
		||||
        'DKIM-Signature: v=1; a=rsa-sha256; c=simple/relaxed;',
 | 
			
		||||
        '        d=example.com; s=default;',
 | 
			
		||||
        '        h=from:to:subject;',
 | 
			
		||||
        '        bh=bodyhash=;',
 | 
			
		||||
        '        b=signature',
 | 
			
		||||
        '',
 | 
			
		||||
        'Testing different canonicalization methods.',
 | 
			
		||||
        'Simple header canonicalization preserves whitespace.',
 | 
			
		||||
        'Relaxed body canonicalization normalizes whitespace.',
 | 
			
		||||
        '.',
 | 
			
		||||
        ''
 | 
			
		||||
      ].join('\r\n');
 | 
			
		||||
      
 | 
			
		||||
      socket.write(email);
 | 
			
		||||
      await waitForResponse(socket, '250');
 | 
			
		||||
      
 | 
			
		||||
      console.log('Email with different canonicalization 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('RFC 6376 DKIM - Long header fields and folding', 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');
 | 
			
		||||
      
 | 
			
		||||
      // Send MAIL FROM
 | 
			
		||||
      socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
      await waitForResponse(socket, '250');
 | 
			
		||||
      
 | 
			
		||||
      // Send RCPT TO
 | 
			
		||||
      socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
			
		||||
      await waitForResponse(socket, '250');
 | 
			
		||||
      
 | 
			
		||||
      // Send DATA
 | 
			
		||||
      socket.write('DATA\r\n');
 | 
			
		||||
      await waitForResponse(socket, '354');
 | 
			
		||||
      
 | 
			
		||||
      // DKIM signature with long fields that require folding
 | 
			
		||||
      const longSignature = 'b=' + 'A'.repeat(200);
 | 
			
		||||
      
 | 
			
		||||
      const email = [
 | 
			
		||||
        `From: sender@example.com`,
 | 
			
		||||
        `To: recipient@example.com`,
 | 
			
		||||
        `Subject: DKIM Long Fields Test`,
 | 
			
		||||
        `Date: ${new Date().toUTCString()}`,
 | 
			
		||||
        `Message-ID: <long-dkim-${Date.now()}@example.com>`,
 | 
			
		||||
        'DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;',
 | 
			
		||||
        '        d=example.com; s=default; t=' + Math.floor(Date.now() / 1000) + ';',
 | 
			
		||||
        '        h=from:to:subject:date:message-id:content-type:mime-version;',
 | 
			
		||||
        '        bh=verylongbodyhashvalueherethatexceedsnormallength1234567890=;',
 | 
			
		||||
        '        ' + longSignature.substring(0, 70),
 | 
			
		||||
        '        ' + longSignature.substring(70, 140),
 | 
			
		||||
        '        ' + longSignature.substring(140),
 | 
			
		||||
        '',
 | 
			
		||||
        'Testing DKIM with long header fields.',
 | 
			
		||||
        '.',
 | 
			
		||||
        ''
 | 
			
		||||
      ].join('\r\n');
 | 
			
		||||
      
 | 
			
		||||
      socket.write(email);
 | 
			
		||||
      await waitForResponse(socket, '250');
 | 
			
		||||
      
 | 
			
		||||
      console.log('Email with long DKIM fields 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('RFC 6376 DKIM - Authentication-Results header', 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');
 | 
			
		||||
      const ehloResponse = await waitForResponse(socket, '250');
 | 
			
		||||
      
 | 
			
		||||
      // Check if server advertises DKIM support
 | 
			
		||||
      const advertisesDkim = ehloResponse.toLowerCase().includes('dkim');
 | 
			
		||||
      console.log('Server advertises DKIM:', advertisesDkim);
 | 
			
		||||
      
 | 
			
		||||
      // Send MAIL FROM
 | 
			
		||||
      socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
      await waitForResponse(socket, '250');
 | 
			
		||||
      
 | 
			
		||||
      // Send RCPT TO
 | 
			
		||||
      socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
			
		||||
      await waitForResponse(socket, '250');
 | 
			
		||||
      
 | 
			
		||||
      // Send DATA
 | 
			
		||||
      socket.write('DATA\r\n');
 | 
			
		||||
      await waitForResponse(socket, '354');
 | 
			
		||||
      
 | 
			
		||||
      // Email to test if server adds Authentication-Results header
 | 
			
		||||
      const email = [
 | 
			
		||||
        `From: sender@example.com`,
 | 
			
		||||
        `To: recipient@example.com`,
 | 
			
		||||
        `Subject: Authentication-Results Test`,
 | 
			
		||||
        `Date: ${new Date().toUTCString()}`,
 | 
			
		||||
        `Message-ID: <auth-results-${Date.now()}@example.com>`,
 | 
			
		||||
        'DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;',
 | 
			
		||||
        '        d=example.com; s=default;',
 | 
			
		||||
        '        h=from:to:subject;',
 | 
			
		||||
        '        bh=simplehash=;',
 | 
			
		||||
        '        b=simplesignature',
 | 
			
		||||
        '',
 | 
			
		||||
        'Testing if server adds Authentication-Results header.',
 | 
			
		||||
        '.',
 | 
			
		||||
        ''
 | 
			
		||||
      ].join('\r\n');
 | 
			
		||||
      
 | 
			
		||||
      socket.write(email);
 | 
			
		||||
      await waitForResponse(socket, '250');
 | 
			
		||||
      
 | 
			
		||||
      console.log('Email accepted - server should process DKIM and potentially add Authentication-Results');
 | 
			
		||||
      
 | 
			
		||||
      // 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();
 | 
			
		||||
		Reference in New Issue
	
	Block a user