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,404 @@
 | 
			
		||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
 | 
			
		||||
import * as net from 'net';
 | 
			
		||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.ts';
 | 
			
		||||
 | 
			
		||||
const TEST_PORT = 2525;
 | 
			
		||||
const TEST_TIMEOUT = 30000;
 | 
			
		||||
 | 
			
		||||
let testServer: ITestServer;
 | 
			
		||||
 | 
			
		||||
tap.test('setup - start test server', async () => {
 | 
			
		||||
  testServer = await startTestServer({ port: TEST_PORT });
 | 
			
		||||
  await new Promise(resolve => setTimeout(resolve, 1000));
 | 
			
		||||
  expect(testServer).toBeDefined();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('Extremely Long Headers - should handle single extremely long header', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  try {
 | 
			
		||||
    const socket = net.createConnection({
 | 
			
		||||
      host: 'localhost',
 | 
			
		||||
      port: TEST_PORT,
 | 
			
		||||
      timeout: TEST_TIMEOUT
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    await new Promise<void>((resolve, reject) => {
 | 
			
		||||
      socket.once('connect', () => resolve());
 | 
			
		||||
      socket.once('error', reject);
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // Get banner
 | 
			
		||||
    await new Promise<string>((resolve) => {
 | 
			
		||||
      socket.once('data', (chunk) => resolve(chunk.toString()));
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // Send EHLO
 | 
			
		||||
    socket.write('EHLO testclient\r\n');
 | 
			
		||||
    await new Promise<string>((resolve) => {
 | 
			
		||||
      let data = '';
 | 
			
		||||
      const handler = (chunk: Buffer) => {
 | 
			
		||||
        data += chunk.toString();
 | 
			
		||||
        if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) {
 | 
			
		||||
          socket.removeListener('data', handler);
 | 
			
		||||
          resolve(data);
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
      socket.on('data', handler);
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // Send MAIL FROM
 | 
			
		||||
    socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
    const mailResponse = await new Promise<string>((resolve) => {
 | 
			
		||||
      socket.once('data', (chunk) => resolve(chunk.toString()));
 | 
			
		||||
    });
 | 
			
		||||
    expect(mailResponse).toInclude('250');
 | 
			
		||||
    
 | 
			
		||||
    // Send RCPT TO
 | 
			
		||||
    socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
			
		||||
    const rcptResponse = await new Promise<string>((resolve) => {
 | 
			
		||||
      socket.once('data', (chunk) => resolve(chunk.toString()));
 | 
			
		||||
    });
 | 
			
		||||
    expect(rcptResponse).toInclude('250');
 | 
			
		||||
    
 | 
			
		||||
    // Send DATA
 | 
			
		||||
    socket.write('DATA\r\n');
 | 
			
		||||
    const dataResponse = await new Promise<string>((resolve) => {
 | 
			
		||||
      socket.once('data', (chunk) => resolve(chunk.toString()));
 | 
			
		||||
    });
 | 
			
		||||
    expect(dataResponse).toInclude('354');
 | 
			
		||||
    
 | 
			
		||||
    // Send email with extremely long header (3000 characters)
 | 
			
		||||
    const longValue = 'X'.repeat(3000);
 | 
			
		||||
    const emailContent = [
 | 
			
		||||
      `Subject: Test Email`,
 | 
			
		||||
      `From: sender@example.com`,
 | 
			
		||||
      `To: recipient@example.com`,
 | 
			
		||||
      `X-Long-Header: ${longValue}`,
 | 
			
		||||
      '',
 | 
			
		||||
      'This email has an extremely long header.',
 | 
			
		||||
      '.',
 | 
			
		||||
      ''
 | 
			
		||||
    ].join('\r\n');
 | 
			
		||||
    
 | 
			
		||||
    socket.write(emailContent);
 | 
			
		||||
    
 | 
			
		||||
    const finalResponse = await new Promise<string>((resolve) => {
 | 
			
		||||
      socket.once('data', (chunk) => resolve(chunk.toString()));
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // Server might accept or reject - both are valid for extremely long headers
 | 
			
		||||
    const accepted = finalResponse.includes('250');
 | 
			
		||||
    const rejected = finalResponse.includes('552') || finalResponse.includes('554') || finalResponse.includes('500');
 | 
			
		||||
    
 | 
			
		||||
    console.log(`Long header test ${accepted ? 'accepted' : 'rejected'}: ${finalResponse.trim()}`);
 | 
			
		||||
    expect(accepted || rejected).toEqual(true);
 | 
			
		||||
    
 | 
			
		||||
    // Clean up
 | 
			
		||||
    socket.write('QUIT\r\n');
 | 
			
		||||
    socket.end();
 | 
			
		||||
    
 | 
			
		||||
  } finally {
 | 
			
		||||
    done.resolve();
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('Extremely Long Headers - should handle multi-line header with many segments', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  try {
 | 
			
		||||
    const socket = net.createConnection({
 | 
			
		||||
      host: 'localhost',
 | 
			
		||||
      port: TEST_PORT,
 | 
			
		||||
      timeout: TEST_TIMEOUT
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    await new Promise<void>((resolve, reject) => {
 | 
			
		||||
      socket.once('connect', () => resolve());
 | 
			
		||||
      socket.once('error', reject);
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // Get banner
 | 
			
		||||
    await new Promise<string>((resolve) => {
 | 
			
		||||
      socket.once('data', (chunk) => resolve(chunk.toString()));
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // Send EHLO
 | 
			
		||||
    socket.write('EHLO testclient\r\n');
 | 
			
		||||
    await new Promise<string>((resolve) => {
 | 
			
		||||
      let data = '';
 | 
			
		||||
      const handler = (chunk: Buffer) => {
 | 
			
		||||
        data += chunk.toString();
 | 
			
		||||
        if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) {
 | 
			
		||||
          socket.removeListener('data', handler);
 | 
			
		||||
          resolve(data);
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
      socket.on('data', handler);
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // Send MAIL FROM
 | 
			
		||||
    socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
    const mailResponse = await new Promise<string>((resolve) => {
 | 
			
		||||
      socket.once('data', (chunk) => resolve(chunk.toString()));
 | 
			
		||||
    });
 | 
			
		||||
    expect(mailResponse).toInclude('250');
 | 
			
		||||
    
 | 
			
		||||
    // Send RCPT TO
 | 
			
		||||
    socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
			
		||||
    const rcptResponse = await new Promise<string>((resolve) => {
 | 
			
		||||
      socket.once('data', (chunk) => resolve(chunk.toString()));
 | 
			
		||||
    });
 | 
			
		||||
    expect(rcptResponse).toInclude('250');
 | 
			
		||||
    
 | 
			
		||||
    // Send DATA
 | 
			
		||||
    socket.write('DATA\r\n');
 | 
			
		||||
    const dataResponse = await new Promise<string>((resolve) => {
 | 
			
		||||
      socket.once('data', (chunk) => resolve(chunk.toString()));
 | 
			
		||||
    });
 | 
			
		||||
    expect(dataResponse).toInclude('354');
 | 
			
		||||
    
 | 
			
		||||
    // Create multi-line header with 50 segments (RFC 5322 folding)
 | 
			
		||||
    const segments = [];
 | 
			
		||||
    for (let i = 0; i < 50; i++) {
 | 
			
		||||
      segments.push(`  Segment ${i}: ${' '.repeat(60)}value`);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    const emailContent = [
 | 
			
		||||
      `Subject: Test Email`,
 | 
			
		||||
      `From: sender@example.com`,
 | 
			
		||||
      `To: recipient@example.com`,
 | 
			
		||||
      `X-Multi-Line: Initial value`,
 | 
			
		||||
      ...segments,
 | 
			
		||||
      '',
 | 
			
		||||
      'This email has a multi-line header with many segments.',
 | 
			
		||||
      '.',
 | 
			
		||||
      ''
 | 
			
		||||
    ].join('\r\n');
 | 
			
		||||
    
 | 
			
		||||
    socket.write(emailContent);
 | 
			
		||||
    
 | 
			
		||||
    const finalResponse = await new Promise<string>((resolve) => {
 | 
			
		||||
      socket.once('data', (chunk) => resolve(chunk.toString()));
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    const accepted = finalResponse.includes('250');
 | 
			
		||||
    const rejected = finalResponse.includes('552') || finalResponse.includes('554') || finalResponse.includes('500');
 | 
			
		||||
    
 | 
			
		||||
    console.log(`Multi-line header test ${accepted ? 'accepted' : 'rejected'}: ${finalResponse.trim()}`);
 | 
			
		||||
    expect(accepted || rejected).toEqual(true);
 | 
			
		||||
    
 | 
			
		||||
    // Clean up
 | 
			
		||||
    socket.write('QUIT\r\n');
 | 
			
		||||
    socket.end();
 | 
			
		||||
    
 | 
			
		||||
  } finally {
 | 
			
		||||
    done.resolve();
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('Extremely Long Headers - should handle multiple long headers in one email', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  try {
 | 
			
		||||
    const socket = net.createConnection({
 | 
			
		||||
      host: 'localhost',
 | 
			
		||||
      port: TEST_PORT,
 | 
			
		||||
      timeout: TEST_TIMEOUT
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    await new Promise<void>((resolve, reject) => {
 | 
			
		||||
      socket.once('connect', () => resolve());
 | 
			
		||||
      socket.once('error', reject);
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // Get banner
 | 
			
		||||
    await new Promise<string>((resolve) => {
 | 
			
		||||
      socket.once('data', (chunk) => resolve(chunk.toString()));
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // Send EHLO
 | 
			
		||||
    socket.write('EHLO testclient\r\n');
 | 
			
		||||
    await new Promise<string>((resolve) => {
 | 
			
		||||
      let data = '';
 | 
			
		||||
      const handler = (chunk: Buffer) => {
 | 
			
		||||
        data += chunk.toString();
 | 
			
		||||
        if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) {
 | 
			
		||||
          socket.removeListener('data', handler);
 | 
			
		||||
          resolve(data);
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
      socket.on('data', handler);
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // Send MAIL FROM
 | 
			
		||||
    socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
    const mailResponse = await new Promise<string>((resolve) => {
 | 
			
		||||
      socket.once('data', (chunk) => resolve(chunk.toString()));
 | 
			
		||||
    });
 | 
			
		||||
    expect(mailResponse).toInclude('250');
 | 
			
		||||
    
 | 
			
		||||
    // Send RCPT TO
 | 
			
		||||
    socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
			
		||||
    const rcptResponse = await new Promise<string>((resolve) => {
 | 
			
		||||
      socket.once('data', (chunk) => resolve(chunk.toString()));
 | 
			
		||||
    });
 | 
			
		||||
    expect(rcptResponse).toInclude('250');
 | 
			
		||||
    
 | 
			
		||||
    // Send DATA
 | 
			
		||||
    socket.write('DATA\r\n');
 | 
			
		||||
    const dataResponse = await new Promise<string>((resolve) => {
 | 
			
		||||
      socket.once('data', (chunk) => resolve(chunk.toString()));
 | 
			
		||||
    });
 | 
			
		||||
    expect(dataResponse).toInclude('354');
 | 
			
		||||
    
 | 
			
		||||
    // Create multiple long headers
 | 
			
		||||
    const header1 = 'A'.repeat(1000);
 | 
			
		||||
    const header2 = 'B'.repeat(1500);
 | 
			
		||||
    const header3 = 'C'.repeat(2000);
 | 
			
		||||
    
 | 
			
		||||
    const emailContent = [
 | 
			
		||||
      `Subject: Test Email with Multiple Long Headers`,
 | 
			
		||||
      `From: sender@example.com`,
 | 
			
		||||
      `To: recipient@example.com`,
 | 
			
		||||
      `X-Long-Header-1: ${header1}`,
 | 
			
		||||
      `X-Long-Header-2: ${header2}`,
 | 
			
		||||
      `X-Long-Header-3: ${header3}`,
 | 
			
		||||
      '',
 | 
			
		||||
      'This email has multiple long headers.',
 | 
			
		||||
      '.',
 | 
			
		||||
      ''
 | 
			
		||||
    ].join('\r\n');
 | 
			
		||||
    
 | 
			
		||||
    const totalHeaderSize = header1.length + header2.length + header3.length;
 | 
			
		||||
    console.log(`Total header size: ${totalHeaderSize} bytes`);
 | 
			
		||||
    
 | 
			
		||||
    socket.write(emailContent);
 | 
			
		||||
    
 | 
			
		||||
    const finalResponse = await new Promise<string>((resolve) => {
 | 
			
		||||
      socket.once('data', (chunk) => resolve(chunk.toString()));
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    const accepted = finalResponse.includes('250');
 | 
			
		||||
    const rejected = finalResponse.includes('552') || finalResponse.includes('554') || finalResponse.includes('500');
 | 
			
		||||
    
 | 
			
		||||
    console.log(`Multiple long headers test ${accepted ? 'accepted' : 'rejected'}: ${finalResponse.trim()}`);
 | 
			
		||||
    expect(accepted || rejected).toEqual(true);
 | 
			
		||||
    
 | 
			
		||||
    // Clean up
 | 
			
		||||
    socket.write('QUIT\r\n');
 | 
			
		||||
    socket.end();
 | 
			
		||||
    
 | 
			
		||||
  } finally {
 | 
			
		||||
    done.resolve();
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('Extremely Long Headers - should handle header with exactly RFC limit', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  try {
 | 
			
		||||
    const socket = net.createConnection({
 | 
			
		||||
      host: 'localhost',
 | 
			
		||||
      port: TEST_PORT,
 | 
			
		||||
      timeout: TEST_TIMEOUT
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    await new Promise<void>((resolve, reject) => {
 | 
			
		||||
      socket.once('connect', () => resolve());
 | 
			
		||||
      socket.once('error', reject);
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // Get banner
 | 
			
		||||
    await new Promise<string>((resolve) => {
 | 
			
		||||
      socket.once('data', (chunk) => resolve(chunk.toString()));
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // Send EHLO
 | 
			
		||||
    socket.write('EHLO testclient\r\n');
 | 
			
		||||
    await new Promise<string>((resolve) => {
 | 
			
		||||
      let data = '';
 | 
			
		||||
      const handler = (chunk: Buffer) => {
 | 
			
		||||
        data += chunk.toString();
 | 
			
		||||
        if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) {
 | 
			
		||||
          socket.removeListener('data', handler);
 | 
			
		||||
          resolve(data);
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
      socket.on('data', handler);
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // Send MAIL FROM
 | 
			
		||||
    socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
    const mailResponse = await new Promise<string>((resolve) => {
 | 
			
		||||
      socket.once('data', (chunk) => resolve(chunk.toString()));
 | 
			
		||||
    });
 | 
			
		||||
    expect(mailResponse).toInclude('250');
 | 
			
		||||
    
 | 
			
		||||
    // Send RCPT TO
 | 
			
		||||
    socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
			
		||||
    const rcptResponse = await new Promise<string>((resolve) => {
 | 
			
		||||
      socket.once('data', (chunk) => resolve(chunk.toString()));
 | 
			
		||||
    });
 | 
			
		||||
    expect(rcptResponse).toInclude('250');
 | 
			
		||||
    
 | 
			
		||||
    // Send DATA
 | 
			
		||||
    socket.write('DATA\r\n');
 | 
			
		||||
    const dataResponse = await new Promise<string>((resolve) => {
 | 
			
		||||
      socket.once('data', (chunk) => resolve(chunk.toString()));
 | 
			
		||||
    });
 | 
			
		||||
    expect(dataResponse).toInclude('354');
 | 
			
		||||
    
 | 
			
		||||
    // Create header line exactly at RFC 5322 limit (998 chars excluding CRLF)
 | 
			
		||||
    // Header name and colon take some space
 | 
			
		||||
    const headerName = 'X-RFC-Limit';
 | 
			
		||||
    const colonSpace = ': ';
 | 
			
		||||
    const remainingSpace = 998 - headerName.length - colonSpace.length;
 | 
			
		||||
    const headerValue = 'X'.repeat(remainingSpace);
 | 
			
		||||
    
 | 
			
		||||
    const emailContent = [
 | 
			
		||||
      `Subject: Test Email`,
 | 
			
		||||
      `From: sender@example.com`,
 | 
			
		||||
      `To: recipient@example.com`,
 | 
			
		||||
      `${headerName}${colonSpace}${headerValue}`,
 | 
			
		||||
      '',
 | 
			
		||||
      'This email has a header at exactly the RFC limit.',
 | 
			
		||||
      '.',
 | 
			
		||||
      ''
 | 
			
		||||
    ].join('\r\n');
 | 
			
		||||
    
 | 
			
		||||
    socket.write(emailContent);
 | 
			
		||||
    
 | 
			
		||||
    const finalResponse = await new Promise<string>((resolve) => {
 | 
			
		||||
      socket.once('data', (chunk) => resolve(chunk.toString()));
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // This should be accepted since it's exactly at the limit
 | 
			
		||||
    const accepted = finalResponse.includes('250');
 | 
			
		||||
    const rejected = finalResponse.includes('552') || finalResponse.includes('554') || finalResponse.includes('500');
 | 
			
		||||
    
 | 
			
		||||
    console.log(`RFC limit header test ${accepted ? 'accepted' : 'rejected'}: ${finalResponse.trim()}`);
 | 
			
		||||
    expect(accepted || rejected).toEqual(true);
 | 
			
		||||
    
 | 
			
		||||
    // RFC compliant servers should accept headers exactly at the limit
 | 
			
		||||
    if (accepted) {
 | 
			
		||||
      console.log('✓ Server correctly accepts headers at RFC limit');
 | 
			
		||||
    } else {
 | 
			
		||||
      console.log('⚠ Server rejected header at RFC limit (may be overly strict)');
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Clean up
 | 
			
		||||
    socket.write('QUIT\r\n');
 | 
			
		||||
    socket.end();
 | 
			
		||||
    
 | 
			
		||||
  } finally {
 | 
			
		||||
    done.resolve();
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('cleanup - stop test server', async () => {
 | 
			
		||||
  await stopTestServer(testServer);
 | 
			
		||||
  expect(true).toEqual(true);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default tap.start();
 | 
			
		||||
		Reference in New Issue
	
	Block a user