update
This commit is contained in:
		
							
								
								
									
										600
									
								
								test/suite/email-processing/test.attachment-handling.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										600
									
								
								test/suite/email-processing/test.attachment-handling.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,600 @@
 | 
			
		||||
import { tap, expect } from '@push.rocks/tapbundle';
 | 
			
		||||
import * as net from 'net';
 | 
			
		||||
import { startTestServer, stopTestServer } from '../server.loader.js';
 | 
			
		||||
 | 
			
		||||
const TEST_PORT = 2525;
 | 
			
		||||
 | 
			
		||||
let testServer: any;
 | 
			
		||||
 | 
			
		||||
tap.test('setup - start test server', async () => {
 | 
			
		||||
  testServer = await startTestServer();
 | 
			
		||||
  await new Promise(resolve => setTimeout(resolve, 1000));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('Attachment Handling - Multiple file types', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: 30000
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let dataBuffer = '';
 | 
			
		||||
  let step = 'greeting';
 | 
			
		||||
  let completed = false;
 | 
			
		||||
  
 | 
			
		||||
  socket.on('data', (data) => {
 | 
			
		||||
    if (completed) return;
 | 
			
		||||
    
 | 
			
		||||
    dataBuffer += data.toString();
 | 
			
		||||
    console.log('Server response:', data.toString());
 | 
			
		||||
    
 | 
			
		||||
    if (step === 'greeting' && dataBuffer.includes('220 ')) {
 | 
			
		||||
      step = 'ehlo';
 | 
			
		||||
      socket.write('EHLO testclient\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'ehlo' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'mail';
 | 
			
		||||
      socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'mail' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'rcpt';
 | 
			
		||||
      socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'rcpt' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'data';
 | 
			
		||||
      socket.write('DATA\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'data' && dataBuffer.includes('354')) {
 | 
			
		||||
      const boundary = 'attachment-test-boundary-12345';
 | 
			
		||||
      
 | 
			
		||||
      // Create various attachments
 | 
			
		||||
      const textAttachment = 'This is a text attachment content.\nIt has multiple lines.\nAnd special chars: åäö';
 | 
			
		||||
      const jsonAttachment = JSON.stringify({
 | 
			
		||||
        name: 'test',
 | 
			
		||||
        data: [1, 2, 3],
 | 
			
		||||
        unicode: 'ñoño',
 | 
			
		||||
        special: '∑∆≈'
 | 
			
		||||
      }, null, 2);
 | 
			
		||||
      
 | 
			
		||||
      // Minimal PNG (1x1 pixel transparent)
 | 
			
		||||
      const pngBase64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==';
 | 
			
		||||
      
 | 
			
		||||
      // Minimal PDF header
 | 
			
		||||
      const pdfBase64 = 'JVBERi0xLjQKJcOkw7zDtsOVDQo=';
 | 
			
		||||
      
 | 
			
		||||
      const email = [
 | 
			
		||||
        `From: sender@example.com`,
 | 
			
		||||
        `To: recipient@example.com`,
 | 
			
		||||
        `Subject: Attachment Handling Test - Multiple Types`,
 | 
			
		||||
        `Date: ${new Date().toUTCString()}`,
 | 
			
		||||
        `Message-ID: <attachment-test-${Date.now()}@example.com>`,
 | 
			
		||||
        `MIME-Version: 1.0`,
 | 
			
		||||
        `Content-Type: multipart/mixed; boundary="${boundary}"`,
 | 
			
		||||
        '',
 | 
			
		||||
        'This is a multi-part message with various attachments.',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: text/plain; charset=utf-8`,
 | 
			
		||||
        '',
 | 
			
		||||
        'This email tests attachment handling capabilities.',
 | 
			
		||||
        'The server should properly process all attached files.',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: text/plain; charset=utf-8`,
 | 
			
		||||
        `Content-Disposition: attachment; filename="document.txt"`,
 | 
			
		||||
        `Content-Transfer-Encoding: 7bit`,
 | 
			
		||||
        '',
 | 
			
		||||
        textAttachment,
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: application/json; charset=utf-8`,
 | 
			
		||||
        `Content-Disposition: attachment; filename="data.json"`,
 | 
			
		||||
        '',
 | 
			
		||||
        jsonAttachment,
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: image/png`,
 | 
			
		||||
        `Content-Disposition: attachment; filename="image.png"`,
 | 
			
		||||
        `Content-Transfer-Encoding: base64`,
 | 
			
		||||
        '',
 | 
			
		||||
        pngBase64,
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: application/octet-stream`,
 | 
			
		||||
        `Content-Disposition: attachment; filename="binary.bin"`,
 | 
			
		||||
        `Content-Transfer-Encoding: base64`,
 | 
			
		||||
        '',
 | 
			
		||||
        Buffer.from('Binary file content with null bytes\0\0\0').toString('base64'),
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: text/csv`,
 | 
			
		||||
        `Content-Disposition: attachment; filename="spreadsheet.csv"`,
 | 
			
		||||
        '',
 | 
			
		||||
        'Name,Age,Country',
 | 
			
		||||
        'Alice,25,Sweden',
 | 
			
		||||
        'Bob,30,Norway',
 | 
			
		||||
        'Charlie,35,Denmark',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: application/xml; charset=utf-8`,
 | 
			
		||||
        `Content-Disposition: attachment; filename="config.xml"`,
 | 
			
		||||
        '',
 | 
			
		||||
        '<?xml version="1.0" encoding="UTF-8"?>',
 | 
			
		||||
        '<config>',
 | 
			
		||||
        '  <setting name="test">value</setting>',
 | 
			
		||||
        '  <unicode>ñoño ∑∆≈</unicode>',
 | 
			
		||||
        '</config>',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: application/pdf`,
 | 
			
		||||
        `Content-Disposition: attachment; filename="document.pdf"`,
 | 
			
		||||
        `Content-Transfer-Encoding: base64`,
 | 
			
		||||
        '',
 | 
			
		||||
        pdfBase64,
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: text/html; charset=utf-8`,
 | 
			
		||||
        `Content-Disposition: attachment; filename="webpage.html"`,
 | 
			
		||||
        '',
 | 
			
		||||
        '<!DOCTYPE html>',
 | 
			
		||||
        '<html><head><title>Test</title></head>',
 | 
			
		||||
        '<body><h1>HTML Attachment</h1><p>Content with <em>markup</em></p></body>',
 | 
			
		||||
        '</html>',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}--`,
 | 
			
		||||
        '.',
 | 
			
		||||
        ''
 | 
			
		||||
      ].join('\r\n');
 | 
			
		||||
      
 | 
			
		||||
      console.log('Sending email with 8 different attachment types');
 | 
			
		||||
      socket.write(email);
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
      step = 'sent';
 | 
			
		||||
    } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
 | 
			
		||||
      if (!completed) {
 | 
			
		||||
        completed = true;
 | 
			
		||||
        console.log('Email with multiple attachments accepted successfully');
 | 
			
		||||
        expect(true).toBeTrue();
 | 
			
		||||
        
 | 
			
		||||
        socket.write('QUIT\r\n');
 | 
			
		||||
        socket.end();
 | 
			
		||||
        done.resolve();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('error', (err) => {
 | 
			
		||||
    console.error('Socket error:', err);
 | 
			
		||||
    done.reject(err);
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  await done.promise;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('Attachment Handling - Large attachment', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: 30000
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let dataBuffer = '';
 | 
			
		||||
  let step = 'greeting';
 | 
			
		||||
  let completed = false;
 | 
			
		||||
  
 | 
			
		||||
  socket.on('data', (data) => {
 | 
			
		||||
    if (completed) return;
 | 
			
		||||
    
 | 
			
		||||
    dataBuffer += data.toString();
 | 
			
		||||
    console.log('Server response:', data.toString());
 | 
			
		||||
    
 | 
			
		||||
    if (step === 'greeting' && dataBuffer.includes('220 ')) {
 | 
			
		||||
      step = 'ehlo';
 | 
			
		||||
      socket.write('EHLO testclient\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'ehlo' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'mail';
 | 
			
		||||
      socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'mail' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'rcpt';
 | 
			
		||||
      socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'rcpt' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'data';
 | 
			
		||||
      socket.write('DATA\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'data' && dataBuffer.includes('354')) {
 | 
			
		||||
      const boundary = 'large-attachment-boundary';
 | 
			
		||||
      
 | 
			
		||||
      // Create a 100KB attachment
 | 
			
		||||
      const largeData = 'A'.repeat(100000);
 | 
			
		||||
      const largeBase64 = Buffer.from(largeData).toString('base64');
 | 
			
		||||
      
 | 
			
		||||
      const email = [
 | 
			
		||||
        `From: sender@example.com`,
 | 
			
		||||
        `To: recipient@example.com`,
 | 
			
		||||
        `Subject: Large Attachment Test`,
 | 
			
		||||
        `Date: ${new Date().toUTCString()}`,
 | 
			
		||||
        `Message-ID: <large-attach-${Date.now()}@example.com>`,
 | 
			
		||||
        `MIME-Version: 1.0`,
 | 
			
		||||
        `Content-Type: multipart/mixed; boundary="${boundary}"`,
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: text/plain`,
 | 
			
		||||
        '',
 | 
			
		||||
        'This email contains a large attachment.',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: application/octet-stream`,
 | 
			
		||||
        `Content-Disposition: attachment; filename="large-file.dat"`,
 | 
			
		||||
        `Content-Transfer-Encoding: base64`,
 | 
			
		||||
        '',
 | 
			
		||||
        largeBase64,
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}--`,
 | 
			
		||||
        '.',
 | 
			
		||||
        ''
 | 
			
		||||
      ].join('\r\n');
 | 
			
		||||
      
 | 
			
		||||
      console.log('Sending email with 100KB attachment');
 | 
			
		||||
      socket.write(email);
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
      step = 'sent';
 | 
			
		||||
    } else if (step === 'sent' && (dataBuffer.includes('250 ') || dataBuffer.includes('552 '))) {
 | 
			
		||||
      if (!completed) {
 | 
			
		||||
        completed = true;
 | 
			
		||||
        const accepted = dataBuffer.includes('250');
 | 
			
		||||
        const rejected = dataBuffer.includes('552'); // Size exceeded
 | 
			
		||||
        
 | 
			
		||||
        console.log(`Large attachment: ${accepted ? 'accepted' : 'rejected (size limit)'}`);
 | 
			
		||||
        expect(accepted || rejected).toBeTrue();
 | 
			
		||||
        
 | 
			
		||||
        socket.write('QUIT\r\n');
 | 
			
		||||
        socket.end();
 | 
			
		||||
        done.resolve();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('error', (err) => {
 | 
			
		||||
    console.error('Socket error:', err);
 | 
			
		||||
    done.reject(err);
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  await done.promise;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('Attachment Handling - Inline vs attachment disposition', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: 30000
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let dataBuffer = '';
 | 
			
		||||
  let step = 'greeting';
 | 
			
		||||
  let completed = false;
 | 
			
		||||
  
 | 
			
		||||
  socket.on('data', (data) => {
 | 
			
		||||
    if (completed) return;
 | 
			
		||||
    
 | 
			
		||||
    dataBuffer += data.toString();
 | 
			
		||||
    console.log('Server response:', data.toString());
 | 
			
		||||
    
 | 
			
		||||
    if (step === 'greeting' && dataBuffer.includes('220 ')) {
 | 
			
		||||
      step = 'ehlo';
 | 
			
		||||
      socket.write('EHLO testclient\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'ehlo' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'mail';
 | 
			
		||||
      socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'mail' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'rcpt';
 | 
			
		||||
      socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'rcpt' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'data';
 | 
			
		||||
      socket.write('DATA\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'data' && dataBuffer.includes('354')) {
 | 
			
		||||
      const boundary = 'inline-attachment-boundary';
 | 
			
		||||
      
 | 
			
		||||
      const email = [
 | 
			
		||||
        `From: sender@example.com`,
 | 
			
		||||
        `To: recipient@example.com`,
 | 
			
		||||
        `Subject: Inline vs Attachment Test`,
 | 
			
		||||
        `Date: ${new Date().toUTCString()}`,
 | 
			
		||||
        `Message-ID: <inline-test-${Date.now()}@example.com>`,
 | 
			
		||||
        `MIME-Version: 1.0`,
 | 
			
		||||
        `Content-Type: multipart/related; boundary="${boundary}"`,
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: text/html`,
 | 
			
		||||
        '',
 | 
			
		||||
        '<html><body>',
 | 
			
		||||
        '<p>This email has inline images:</p>',
 | 
			
		||||
        '<img src="cid:image1">',
 | 
			
		||||
        '<img src="cid:image2">',
 | 
			
		||||
        '</body></html>',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: image/png`,
 | 
			
		||||
        `Content-ID: <image1>`,
 | 
			
		||||
        `Content-Disposition: inline; filename="inline1.png"`,
 | 
			
		||||
        `Content-Transfer-Encoding: base64`,
 | 
			
		||||
        '',
 | 
			
		||||
        'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: image/png`,
 | 
			
		||||
        `Content-ID: <image2>`,
 | 
			
		||||
        `Content-Disposition: inline; filename="inline2.png"`,
 | 
			
		||||
        `Content-Transfer-Encoding: base64`,
 | 
			
		||||
        '',
 | 
			
		||||
        'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: application/pdf`,
 | 
			
		||||
        `Content-Disposition: attachment; filename="document.pdf"`,
 | 
			
		||||
        `Content-Transfer-Encoding: base64`,
 | 
			
		||||
        '',
 | 
			
		||||
        'JVBERi0xLjQKJcOkw7zDtsOVDQo=',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}--`,
 | 
			
		||||
        '.',
 | 
			
		||||
        ''
 | 
			
		||||
      ].join('\r\n');
 | 
			
		||||
      
 | 
			
		||||
      socket.write(email);
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
      step = 'sent';
 | 
			
		||||
    } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
 | 
			
		||||
      if (!completed) {
 | 
			
		||||
        completed = true;
 | 
			
		||||
        console.log('Email with inline and attachment dispositions accepted');
 | 
			
		||||
        expect(true).toBeTrue();
 | 
			
		||||
        
 | 
			
		||||
        socket.write('QUIT\r\n');
 | 
			
		||||
        socket.end();
 | 
			
		||||
        done.resolve();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('error', (err) => {
 | 
			
		||||
    console.error('Socket error:', err);
 | 
			
		||||
    done.reject(err);
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  await done.promise;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('Attachment Handling - Filename encoding', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: 30000
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let dataBuffer = '';
 | 
			
		||||
  let step = 'greeting';
 | 
			
		||||
  let completed = false;
 | 
			
		||||
  
 | 
			
		||||
  socket.on('data', (data) => {
 | 
			
		||||
    if (completed) return;
 | 
			
		||||
    
 | 
			
		||||
    dataBuffer += data.toString();
 | 
			
		||||
    console.log('Server response:', data.toString());
 | 
			
		||||
    
 | 
			
		||||
    if (step === 'greeting' && dataBuffer.includes('220 ')) {
 | 
			
		||||
      step = 'ehlo';
 | 
			
		||||
      socket.write('EHLO testclient\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'ehlo' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'mail';
 | 
			
		||||
      socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'mail' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'rcpt';
 | 
			
		||||
      socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'rcpt' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'data';
 | 
			
		||||
      socket.write('DATA\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'data' && dataBuffer.includes('354')) {
 | 
			
		||||
      const boundary = 'filename-encoding-boundary';
 | 
			
		||||
      
 | 
			
		||||
      const email = [
 | 
			
		||||
        `From: sender@example.com`,
 | 
			
		||||
        `To: recipient@example.com`,
 | 
			
		||||
        `Subject: Filename Encoding Test`,
 | 
			
		||||
        `Date: ${new Date().toUTCString()}`,
 | 
			
		||||
        `Message-ID: <filename-test-${Date.now()}@example.com>`,
 | 
			
		||||
        `MIME-Version: 1.0`,
 | 
			
		||||
        `Content-Type: multipart/mixed; boundary="${boundary}"`,
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: text/plain`,
 | 
			
		||||
        '',
 | 
			
		||||
        'Testing various filename encodings.',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: text/plain`,
 | 
			
		||||
        `Content-Disposition: attachment; filename="simple.txt"`,
 | 
			
		||||
        '',
 | 
			
		||||
        'Simple ASCII filename',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: text/plain`,
 | 
			
		||||
        `Content-Disposition: attachment; filename="åäö-nordic.txt"`,
 | 
			
		||||
        '',
 | 
			
		||||
        'Nordic characters in filename',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: text/plain`,
 | 
			
		||||
        `Content-Disposition: attachment; filename*=UTF-8''%C3%A5%C3%A4%C3%B6-encoded.txt`,
 | 
			
		||||
        '',
 | 
			
		||||
        'RFC 2231 encoded filename',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: text/plain`,
 | 
			
		||||
        `Content-Disposition: attachment; filename="=?UTF-8?B?8J+YgC1lbW9qaS50eHQ=?="`,
 | 
			
		||||
        '',
 | 
			
		||||
        'MIME encoded filename with emoji',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: text/plain`,
 | 
			
		||||
        `Content-Disposition: attachment; filename="very long filename that exceeds normal limits and should be handled properly by the server.txt"`,
 | 
			
		||||
        '',
 | 
			
		||||
        'Very long filename',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}--`,
 | 
			
		||||
        '.',
 | 
			
		||||
        ''
 | 
			
		||||
      ].join('\r\n');
 | 
			
		||||
      
 | 
			
		||||
      socket.write(email);
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
      step = 'sent';
 | 
			
		||||
    } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
 | 
			
		||||
      if (!completed) {
 | 
			
		||||
        completed = true;
 | 
			
		||||
        console.log('Email with various filename encodings accepted');
 | 
			
		||||
        expect(true).toBeTrue();
 | 
			
		||||
        
 | 
			
		||||
        socket.write('QUIT\r\n');
 | 
			
		||||
        socket.end();
 | 
			
		||||
        done.resolve();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('error', (err) => {
 | 
			
		||||
    console.error('Socket error:', err);
 | 
			
		||||
    done.reject(err);
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  await done.promise;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('Attachment Handling - Empty and malformed attachments', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: 30000
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let dataBuffer = '';
 | 
			
		||||
  let step = 'greeting';
 | 
			
		||||
  let completed = false;
 | 
			
		||||
  
 | 
			
		||||
  socket.on('data', (data) => {
 | 
			
		||||
    if (completed) return;
 | 
			
		||||
    
 | 
			
		||||
    dataBuffer += data.toString();
 | 
			
		||||
    console.log('Server response:', data.toString());
 | 
			
		||||
    
 | 
			
		||||
    if (step === 'greeting' && dataBuffer.includes('220 ')) {
 | 
			
		||||
      step = 'ehlo';
 | 
			
		||||
      socket.write('EHLO testclient\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'ehlo' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'mail';
 | 
			
		||||
      socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'mail' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'rcpt';
 | 
			
		||||
      socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'rcpt' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'data';
 | 
			
		||||
      socket.write('DATA\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'data' && dataBuffer.includes('354')) {
 | 
			
		||||
      const boundary = 'malformed-boundary';
 | 
			
		||||
      
 | 
			
		||||
      const email = [
 | 
			
		||||
        `From: sender@example.com`,
 | 
			
		||||
        `To: recipient@example.com`,
 | 
			
		||||
        `Subject: Empty and Malformed Attachments`,
 | 
			
		||||
        `Date: ${new Date().toUTCString()}`,
 | 
			
		||||
        `Message-ID: <malformed-${Date.now()}@example.com>`,
 | 
			
		||||
        `MIME-Version: 1.0`,
 | 
			
		||||
        `Content-Type: multipart/mixed; boundary="${boundary}"`,
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: text/plain`,
 | 
			
		||||
        '',
 | 
			
		||||
        'Testing empty and malformed attachments.',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: application/octet-stream`,
 | 
			
		||||
        `Content-Disposition: attachment; filename="empty.dat"`,
 | 
			
		||||
        '',
 | 
			
		||||
        '', // Empty attachment
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: text/plain`,
 | 
			
		||||
        `Content-Disposition: attachment`, // Missing filename
 | 
			
		||||
        '',
 | 
			
		||||
        'Attachment without filename',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: image/png`,
 | 
			
		||||
        `Content-Disposition: attachment; filename="broken.png"`,
 | 
			
		||||
        `Content-Transfer-Encoding: base64`,
 | 
			
		||||
        '',
 | 
			
		||||
        'NOT-VALID-BASE64-@#$%', // Invalid base64
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Disposition: attachment; filename="no-content-type.txt"`, // Missing Content-Type
 | 
			
		||||
        '',
 | 
			
		||||
        'Attachment without Content-Type header',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}--`,
 | 
			
		||||
        '.',
 | 
			
		||||
        ''
 | 
			
		||||
      ].join('\r\n');
 | 
			
		||||
      
 | 
			
		||||
      socket.write(email);
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
      step = 'sent';
 | 
			
		||||
    } else if (step === 'sent' && (dataBuffer.includes('250 ') || dataBuffer.includes('550 '))) {
 | 
			
		||||
      if (!completed) {
 | 
			
		||||
        completed = true;
 | 
			
		||||
        const result = dataBuffer.includes('250') ? 'accepted' : 'rejected';
 | 
			
		||||
        console.log(`Email with malformed attachments ${result}`);
 | 
			
		||||
        expect(true).toBeTrue();
 | 
			
		||||
        
 | 
			
		||||
        socket.write('QUIT\r\n');
 | 
			
		||||
        socket.end();
 | 
			
		||||
        done.resolve();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('error', (err) => {
 | 
			
		||||
    console.error('Socket error:', err);
 | 
			
		||||
    done.reject(err);
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  await done.promise;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('cleanup - stop test server', async () => {
 | 
			
		||||
  await stopTestServer();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.start();
 | 
			
		||||
							
								
								
									
										338
									
								
								test/suite/email-processing/test.basic-email-sending.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										338
									
								
								test/suite/email-processing/test.basic-email-sending.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,338 @@
 | 
			
		||||
import { tap, expect } from '@push.rocks/tapbundle';
 | 
			
		||||
import * as net from 'net';
 | 
			
		||||
import { startTestServer, stopTestServer } from '../server.loader.js';
 | 
			
		||||
 | 
			
		||||
// Test configuration
 | 
			
		||||
const TEST_PORT = 2525;
 | 
			
		||||
const TEST_TIMEOUT = 15000;
 | 
			
		||||
 | 
			
		||||
let testServer: any;
 | 
			
		||||
 | 
			
		||||
// Setup
 | 
			
		||||
tap.test('setup - start SMTP server', async () => {
 | 
			
		||||
  testServer = await startTestServer();
 | 
			
		||||
  await new Promise(resolve => setTimeout(resolve, 1000));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Test: Complete email sending flow
 | 
			
		||||
tap.test('Basic Email Sending - should send email through complete SMTP flow', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: TEST_TIMEOUT
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let receivedData = '';
 | 
			
		||||
  let currentStep = 'connecting';
 | 
			
		||||
  const fromAddress = 'sender@example.com';
 | 
			
		||||
  const toAddress = 'recipient@example.com';
 | 
			
		||||
  const emailContent = `Subject: Production Test Email\r\nFrom: ${fromAddress}\r\nTo: ${toAddress}\r\nDate: ${new Date().toUTCString()}\r\n\r\nThis is a test email sent during production testing.\r\nTest ID: EP-01\r\nTimestamp: ${Date.now()}\r\n`;
 | 
			
		||||
  
 | 
			
		||||
  const steps: string[] = [];
 | 
			
		||||
  
 | 
			
		||||
  socket.on('data', (data) => {
 | 
			
		||||
    receivedData += data.toString();
 | 
			
		||||
    
 | 
			
		||||
    if (currentStep === 'connecting' && receivedData.includes('220')) {
 | 
			
		||||
      steps.push('CONNECT');
 | 
			
		||||
      currentStep = 'ehlo';
 | 
			
		||||
      socket.write('EHLO test.example.com\r\n');
 | 
			
		||||
    } else if (currentStep === 'ehlo' && receivedData.includes('250')) {
 | 
			
		||||
      steps.push('EHLO');
 | 
			
		||||
      currentStep = 'mail_from';
 | 
			
		||||
      socket.write(`MAIL FROM:<${fromAddress}>\r\n`);
 | 
			
		||||
    } else if (currentStep === 'mail_from' && receivedData.includes('250')) {
 | 
			
		||||
      steps.push('MAIL FROM');
 | 
			
		||||
      currentStep = 'rcpt_to';
 | 
			
		||||
      socket.write(`RCPT TO:<${toAddress}>\r\n`);
 | 
			
		||||
    } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) {
 | 
			
		||||
      steps.push('RCPT TO');
 | 
			
		||||
      currentStep = 'data';
 | 
			
		||||
      socket.write('DATA\r\n');
 | 
			
		||||
    } else if (currentStep === 'data' && receivedData.includes('354')) {
 | 
			
		||||
      steps.push('DATA');
 | 
			
		||||
      currentStep = 'email_content';
 | 
			
		||||
      socket.write(emailContent);
 | 
			
		||||
      socket.write('\r\n.\r\n'); // End of data marker
 | 
			
		||||
    } else if (currentStep === 'email_content' && receivedData.includes('250')) {
 | 
			
		||||
      steps.push('CONTENT');
 | 
			
		||||
      currentStep = 'quit';
 | 
			
		||||
      socket.write('QUIT\r\n');
 | 
			
		||||
    } else if (currentStep === 'quit' && receivedData.includes('221')) {
 | 
			
		||||
      steps.push('QUIT');
 | 
			
		||||
      socket.destroy();
 | 
			
		||||
      
 | 
			
		||||
      // Verify all steps completed
 | 
			
		||||
      expect(steps).toInclude('CONNECT');
 | 
			
		||||
      expect(steps).toInclude('EHLO');
 | 
			
		||||
      expect(steps).toInclude('MAIL FROM');
 | 
			
		||||
      expect(steps).toInclude('RCPT TO');
 | 
			
		||||
      expect(steps).toInclude('DATA');
 | 
			
		||||
      expect(steps).toInclude('CONTENT');
 | 
			
		||||
      expect(steps).toInclude('QUIT');
 | 
			
		||||
      expect(steps.length).toEqual(7);
 | 
			
		||||
      
 | 
			
		||||
      done.resolve();
 | 
			
		||||
    } else if (receivedData.match(/\r\n5\d{2}\s/)) {
 | 
			
		||||
      // Server error (5xx response codes)
 | 
			
		||||
      socket.destroy();
 | 
			
		||||
      done.reject(new Error(`Email sending failed at step ${currentStep}: ${receivedData}`));
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('error', (error) => {
 | 
			
		||||
    done.reject(error);
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('timeout', () => {
 | 
			
		||||
    socket.destroy();
 | 
			
		||||
    done.reject(new Error(`Connection timeout at step: ${currentStep}`));
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  await done.promise;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Test: Send email with attachments (MIME)
 | 
			
		||||
tap.test('Basic Email Sending - should send email with MIME attachment', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: TEST_TIMEOUT
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let receivedData = '';
 | 
			
		||||
  let currentStep = 'connecting';
 | 
			
		||||
  const fromAddress = 'sender@example.com';
 | 
			
		||||
  const toAddress = 'recipient@example.com';
 | 
			
		||||
  const boundary = '----=_Part_0_1234567890';
 | 
			
		||||
  
 | 
			
		||||
  const emailContent = `Subject: Email with Attachment\r\nFrom: ${fromAddress}\r\nTo: ${toAddress}\r\nMIME-Version: 1.0\r\nContent-Type: multipart/mixed; boundary="${boundary}"\r\n\r\n--${boundary}\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\nThis email contains an attachment.\r\n\r\n--${boundary}\r\nContent-Type: text/plain; name="test.txt"\r\nContent-Disposition: attachment; filename="test.txt"\r\nContent-Transfer-Encoding: base64\r\n\r\nVGhpcyBpcyBhIHRlc3QgZmlsZS4=\r\n\r\n--${boundary}--\r\n`;
 | 
			
		||||
  
 | 
			
		||||
  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:<${fromAddress}>\r\n`);
 | 
			
		||||
    } else if (currentStep === 'mail_from' && receivedData.includes('250')) {
 | 
			
		||||
      currentStep = 'rcpt_to';
 | 
			
		||||
      socket.write(`RCPT TO:<${toAddress}>\r\n`);
 | 
			
		||||
    } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) {
 | 
			
		||||
      currentStep = 'data';
 | 
			
		||||
      socket.write('DATA\r\n');
 | 
			
		||||
    } else if (currentStep === 'data' && receivedData.includes('354')) {
 | 
			
		||||
      currentStep = 'email_content';
 | 
			
		||||
      socket.write(emailContent);
 | 
			
		||||
      socket.write('\r\n.\r\n'); // End of data marker
 | 
			
		||||
    } else if (currentStep === 'email_content' && receivedData.includes('250')) {
 | 
			
		||||
      socket.write('QUIT\r\n');
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        socket.destroy();
 | 
			
		||||
        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;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Test: Send HTML email
 | 
			
		||||
tap.test('Basic Email Sending - should send HTML email', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: TEST_TIMEOUT
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let receivedData = '';
 | 
			
		||||
  let currentStep = 'connecting';
 | 
			
		||||
  const fromAddress = 'sender@example.com';
 | 
			
		||||
  const toAddress = 'recipient@example.com';
 | 
			
		||||
  const boundary = '----=_Part_0_987654321';
 | 
			
		||||
  
 | 
			
		||||
  const emailContent = `Subject: HTML Email Test\r\nFrom: ${fromAddress}\r\nTo: ${toAddress}\r\nMIME-Version: 1.0\r\nContent-Type: multipart/alternative; boundary="${boundary}"\r\n\r\n--${boundary}\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\nThis is the plain text version.\r\n\r\n--${boundary}\r\nContent-Type: text/html; charset=UTF-8\r\n\r\n<html><body><h1>HTML Email</h1><p>This is the <strong>HTML</strong> version.</p></body></html>\r\n\r\n--${boundary}--\r\n`;
 | 
			
		||||
  
 | 
			
		||||
  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:<${fromAddress}>\r\n`);
 | 
			
		||||
    } else if (currentStep === 'mail_from' && receivedData.includes('250')) {
 | 
			
		||||
      currentStep = 'rcpt_to';
 | 
			
		||||
      socket.write(`RCPT TO:<${toAddress}>\r\n`);
 | 
			
		||||
    } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) {
 | 
			
		||||
      currentStep = 'data';
 | 
			
		||||
      socket.write('DATA\r\n');
 | 
			
		||||
    } else if (currentStep === 'data' && receivedData.includes('354')) {
 | 
			
		||||
      currentStep = 'email_content';
 | 
			
		||||
      socket.write(emailContent);
 | 
			
		||||
      socket.write('\r\n.\r\n'); // End of data marker
 | 
			
		||||
    } else if (currentStep === 'email_content' && receivedData.includes('250')) {
 | 
			
		||||
      socket.write('QUIT\r\n');
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        socket.destroy();
 | 
			
		||||
        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;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Test: Send email with custom headers
 | 
			
		||||
tap.test('Basic Email Sending - should send email with custom headers', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: TEST_TIMEOUT
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let receivedData = '';
 | 
			
		||||
  let currentStep = 'connecting';
 | 
			
		||||
  const fromAddress = 'sender@example.com';
 | 
			
		||||
  const toAddress = 'recipient@example.com';
 | 
			
		||||
  
 | 
			
		||||
  const emailContent = `Subject: Custom Headers Test\r\nFrom: ${fromAddress}\r\nTo: ${toAddress}\r\nX-Custom-Header: CustomValue\r\nX-Priority: 1\r\nX-Mailer: SMTP Test Suite\r\nReply-To: noreply@example.com\r\nOrganization: Test Organization\r\n\r\nThis email contains custom headers.\r\n`;
 | 
			
		||||
  
 | 
			
		||||
  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:<${fromAddress}>\r\n`);
 | 
			
		||||
    } else if (currentStep === 'mail_from' && receivedData.includes('250')) {
 | 
			
		||||
      currentStep = 'rcpt_to';
 | 
			
		||||
      socket.write(`RCPT TO:<${toAddress}>\r\n`);
 | 
			
		||||
    } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) {
 | 
			
		||||
      currentStep = 'data';
 | 
			
		||||
      socket.write('DATA\r\n');
 | 
			
		||||
    } else if (currentStep === 'data' && receivedData.includes('354')) {
 | 
			
		||||
      currentStep = 'email_content';
 | 
			
		||||
      socket.write(emailContent);
 | 
			
		||||
      socket.write('\r\n.\r\n'); // End of data marker
 | 
			
		||||
    } else if (currentStep === 'email_content' && receivedData.includes('250')) {
 | 
			
		||||
      socket.write('QUIT\r\n');
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        socket.destroy();
 | 
			
		||||
        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;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Test: Minimal email (only required headers)
 | 
			
		||||
tap.test('Basic Email Sending - should send minimal email', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: TEST_TIMEOUT
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let receivedData = '';
 | 
			
		||||
  let currentStep = 'connecting';
 | 
			
		||||
  const fromAddress = 'sender@example.com';
 | 
			
		||||
  const toAddress = 'recipient@example.com';
 | 
			
		||||
  
 | 
			
		||||
  // Minimal email - just a body, no headers
 | 
			
		||||
  const emailContent = 'This is a minimal email with no headers.\r\n';
 | 
			
		||||
  
 | 
			
		||||
  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:<${fromAddress}>\r\n`);
 | 
			
		||||
    } else if (currentStep === 'mail_from' && receivedData.includes('250')) {
 | 
			
		||||
      currentStep = 'rcpt_to';
 | 
			
		||||
      socket.write(`RCPT TO:<${toAddress}>\r\n`);
 | 
			
		||||
    } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) {
 | 
			
		||||
      currentStep = 'data';
 | 
			
		||||
      socket.write('DATA\r\n');
 | 
			
		||||
    } else if (currentStep === 'data' && receivedData.includes('354')) {
 | 
			
		||||
      currentStep = 'email_content';
 | 
			
		||||
      socket.write(emailContent);
 | 
			
		||||
      socket.write('\r\n.\r\n'); // End of data marker
 | 
			
		||||
    } else if (currentStep === 'email_content' && receivedData.includes('250')) {
 | 
			
		||||
      socket.write('QUIT\r\n');
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        socket.destroy();
 | 
			
		||||
        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();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Start the test
 | 
			
		||||
tap.start();
 | 
			
		||||
@@ -0,0 +1,486 @@
 | 
			
		||||
import { tap, expect } from '@push.rocks/tapbundle';
 | 
			
		||||
import * as net from 'net';
 | 
			
		||||
import { startTestServer, stopTestServer } from '../server.loader.js';
 | 
			
		||||
 | 
			
		||||
const TEST_PORT = 2525;
 | 
			
		||||
 | 
			
		||||
let testServer: any;
 | 
			
		||||
 | 
			
		||||
tap.test('setup - start test server', async () => {
 | 
			
		||||
  testServer = await startTestServer();
 | 
			
		||||
  await new Promise(resolve => setTimeout(resolve, 1000));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('DSN - Extension advertised in EHLO', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: 30000
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let dataBuffer = '';
 | 
			
		||||
  
 | 
			
		||||
  socket.on('data', (data) => {
 | 
			
		||||
    dataBuffer += data.toString();
 | 
			
		||||
    console.log('Server response:', data.toString());
 | 
			
		||||
    
 | 
			
		||||
    if (dataBuffer.includes('220 ') && !dataBuffer.includes('EHLO')) {
 | 
			
		||||
      socket.write('EHLO testclient\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (dataBuffer.includes('250')) {
 | 
			
		||||
      // Check if DSN extension is advertised
 | 
			
		||||
      const dsnSupported = dataBuffer.toLowerCase().includes('dsn');
 | 
			
		||||
      console.log('DSN extension advertised:', dsnSupported);
 | 
			
		||||
      
 | 
			
		||||
      // Parse extensions
 | 
			
		||||
      const lines = dataBuffer.split('\r\n');
 | 
			
		||||
      const extensions = lines
 | 
			
		||||
        .filter(line => line.startsWith('250-') || (line.startsWith('250 ') && lines.indexOf(line) > 0))
 | 
			
		||||
        .map(line => line.substring(4).split(' ')[0].toUpperCase());
 | 
			
		||||
      
 | 
			
		||||
      console.log('Server extensions:', extensions);
 | 
			
		||||
      
 | 
			
		||||
      socket.write('QUIT\r\n');
 | 
			
		||||
      socket.end();
 | 
			
		||||
      done.resolve();
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('error', (err) => {
 | 
			
		||||
    console.error('Socket error:', err);
 | 
			
		||||
    done.reject(err);
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  await done.promise;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('DSN - Success notification request', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: 30000
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let dataBuffer = '';
 | 
			
		||||
  let step = 'greeting';
 | 
			
		||||
  let completed = false;
 | 
			
		||||
  
 | 
			
		||||
  socket.on('data', (data) => {
 | 
			
		||||
    dataBuffer += data.toString();
 | 
			
		||||
    console.log('Server response:', data.toString());
 | 
			
		||||
    
 | 
			
		||||
    if (step === 'greeting' && dataBuffer.includes('220 ')) {
 | 
			
		||||
      step = 'ehlo';
 | 
			
		||||
      socket.write('EHLO testclient\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'ehlo' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'mail';
 | 
			
		||||
      // MAIL FROM with DSN parameters
 | 
			
		||||
      const envId = `dsn-success-${Date.now()}`;
 | 
			
		||||
      socket.write(`MAIL FROM:<sender@example.com> RET=FULL ENVID=${envId}\r\n`);
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'mail') {
 | 
			
		||||
      const accepted = dataBuffer.includes('250');
 | 
			
		||||
      const notSupported = dataBuffer.includes('501') || dataBuffer.includes('555');
 | 
			
		||||
      
 | 
			
		||||
      console.log(`MAIL FROM with DSN: ${accepted ? 'accepted' : notSupported ? 'not supported' : 'error'}`);
 | 
			
		||||
      
 | 
			
		||||
      if (accepted || notSupported) {
 | 
			
		||||
        step = 'rcpt';
 | 
			
		||||
        // Plain MAIL FROM if DSN not supported
 | 
			
		||||
        if (notSupported) {
 | 
			
		||||
          socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
          dataBuffer = '';
 | 
			
		||||
        } else {
 | 
			
		||||
          // RCPT TO with NOTIFY parameter
 | 
			
		||||
          socket.write('RCPT TO:<recipient@example.com> NOTIFY=SUCCESS\r\n');
 | 
			
		||||
          dataBuffer = '';
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } else if (step === 'rcpt') {
 | 
			
		||||
      const accepted = dataBuffer.includes('250');
 | 
			
		||||
      const notSupported = dataBuffer.includes('501') || dataBuffer.includes('555');
 | 
			
		||||
      
 | 
			
		||||
      if (notSupported) {
 | 
			
		||||
        // DSN not supported, try plain RCPT TO
 | 
			
		||||
        socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
			
		||||
        step = 'rcpt_plain';
 | 
			
		||||
        dataBuffer = '';
 | 
			
		||||
      } else if (accepted) {
 | 
			
		||||
        step = 'data';
 | 
			
		||||
        socket.write('DATA\r\n');
 | 
			
		||||
        dataBuffer = '';
 | 
			
		||||
      }
 | 
			
		||||
    } else if (step === 'rcpt_plain' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'data';
 | 
			
		||||
      socket.write('DATA\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'data' && dataBuffer.includes('354')) {
 | 
			
		||||
      const email = [
 | 
			
		||||
        `From: sender@example.com`,
 | 
			
		||||
        `To: recipient@example.com`,
 | 
			
		||||
        `Subject: DSN Test - Success Notification`,
 | 
			
		||||
        `Date: ${new Date().toUTCString()}`,
 | 
			
		||||
        `Message-ID: <dsn-success-${Date.now()}@example.com>`,
 | 
			
		||||
        '',
 | 
			
		||||
        'This email tests DSN success notification.',
 | 
			
		||||
        'The server should send a success DSN if supported.',
 | 
			
		||||
        '.',
 | 
			
		||||
        ''
 | 
			
		||||
      ].join('\r\n');
 | 
			
		||||
      
 | 
			
		||||
      socket.write(email);
 | 
			
		||||
      step = 'sent';
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
 | 
			
		||||
      if (!completed) {
 | 
			
		||||
        completed = true;
 | 
			
		||||
        console.log('Email with DSN success request accepted');
 | 
			
		||||
        expect(true).toBeTrue();
 | 
			
		||||
        
 | 
			
		||||
        socket.write('QUIT\r\n');
 | 
			
		||||
        socket.end();
 | 
			
		||||
        done.resolve();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('error', (err) => {
 | 
			
		||||
    console.error('Socket error:', err);
 | 
			
		||||
    done.reject(err);
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  await done.promise;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('DSN - Multiple notification types', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: 30000
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let dataBuffer = '';
 | 
			
		||||
  let step = 'greeting';
 | 
			
		||||
  let completed = false;
 | 
			
		||||
  
 | 
			
		||||
  socket.on('data', (data) => {
 | 
			
		||||
    dataBuffer += data.toString();
 | 
			
		||||
    console.log('Server response:', data.toString());
 | 
			
		||||
    
 | 
			
		||||
    if (step === 'greeting' && dataBuffer.includes('220 ')) {
 | 
			
		||||
      step = 'ehlo';
 | 
			
		||||
      socket.write('EHLO testclient\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'ehlo' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'mail';
 | 
			
		||||
      socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'mail' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'rcpt';
 | 
			
		||||
      // Request multiple notification types
 | 
			
		||||
      socket.write('RCPT TO:<recipient@example.com> NOTIFY=SUCCESS,FAILURE,DELAY\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'rcpt') {
 | 
			
		||||
      const accepted = dataBuffer.includes('250');
 | 
			
		||||
      const notSupported = dataBuffer.includes('501') || dataBuffer.includes('555');
 | 
			
		||||
      
 | 
			
		||||
      console.log(`Multiple NOTIFY types: ${accepted ? 'accepted' : notSupported ? 'not supported' : 'error'}`);
 | 
			
		||||
      
 | 
			
		||||
      if (notSupported) {
 | 
			
		||||
        // Try plain RCPT TO
 | 
			
		||||
        socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
			
		||||
        step = 'rcpt_plain';
 | 
			
		||||
        dataBuffer = '';
 | 
			
		||||
      } else if (accepted) {
 | 
			
		||||
        step = 'data';
 | 
			
		||||
        socket.write('DATA\r\n');
 | 
			
		||||
        dataBuffer = '';
 | 
			
		||||
      }
 | 
			
		||||
    } else if (step === 'rcpt_plain' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'data';
 | 
			
		||||
      socket.write('DATA\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'data' && dataBuffer.includes('354')) {
 | 
			
		||||
      const email = [
 | 
			
		||||
        `From: sender@example.com`,
 | 
			
		||||
        `To: recipient@example.com`,
 | 
			
		||||
        `Subject: DSN Test - Multiple Notifications`,
 | 
			
		||||
        `Date: ${new Date().toUTCString()}`,
 | 
			
		||||
        `Message-ID: <dsn-multi-${Date.now()}@example.com>`,
 | 
			
		||||
        '',
 | 
			
		||||
        'Testing multiple DSN notification types.',
 | 
			
		||||
        '.',
 | 
			
		||||
        ''
 | 
			
		||||
      ].join('\r\n');
 | 
			
		||||
      
 | 
			
		||||
      socket.write(email);
 | 
			
		||||
      step = 'sent';
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
 | 
			
		||||
      if (!completed) {
 | 
			
		||||
        completed = true;
 | 
			
		||||
        console.log('Email with multiple DSN types accepted');
 | 
			
		||||
        expect(true).toBeTrue();
 | 
			
		||||
        
 | 
			
		||||
        socket.write('QUIT\r\n');
 | 
			
		||||
        socket.end();
 | 
			
		||||
        done.resolve();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('error', (err) => {
 | 
			
		||||
    console.error('Socket error:', err);
 | 
			
		||||
    done.reject(err);
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  await done.promise;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('DSN - Never notify', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: 30000
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let dataBuffer = '';
 | 
			
		||||
  let step = 'greeting';
 | 
			
		||||
  let completed = false;
 | 
			
		||||
  
 | 
			
		||||
  socket.on('data', (data) => {
 | 
			
		||||
    dataBuffer += data.toString();
 | 
			
		||||
    console.log('Server response:', data.toString());
 | 
			
		||||
    
 | 
			
		||||
    if (step === 'greeting' && dataBuffer.includes('220 ')) {
 | 
			
		||||
      step = 'ehlo';
 | 
			
		||||
      socket.write('EHLO testclient\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'ehlo' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'mail';
 | 
			
		||||
      socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'mail' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'rcpt';
 | 
			
		||||
      // Request no notifications
 | 
			
		||||
      socket.write('RCPT TO:<recipient@example.com> NOTIFY=NEVER\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'rcpt') {
 | 
			
		||||
      const accepted = dataBuffer.includes('250');
 | 
			
		||||
      const notSupported = dataBuffer.includes('501') || dataBuffer.includes('555');
 | 
			
		||||
      
 | 
			
		||||
      console.log(`NOTIFY=NEVER: ${accepted ? 'accepted' : notSupported ? 'not supported' : 'error'}`);
 | 
			
		||||
      expect(accepted || notSupported).toBeTrue();
 | 
			
		||||
      
 | 
			
		||||
      if (notSupported) {
 | 
			
		||||
        socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
			
		||||
        step = 'rcpt_plain';
 | 
			
		||||
        dataBuffer = '';
 | 
			
		||||
      } else if (accepted) {
 | 
			
		||||
        step = 'data';
 | 
			
		||||
        socket.write('DATA\r\n');
 | 
			
		||||
        dataBuffer = '';
 | 
			
		||||
      }
 | 
			
		||||
    } else if (step === 'rcpt_plain' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'data';
 | 
			
		||||
      socket.write('DATA\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'data' && dataBuffer.includes('354')) {
 | 
			
		||||
      const email = [
 | 
			
		||||
        `From: sender@example.com`,
 | 
			
		||||
        `To: recipient@example.com`,
 | 
			
		||||
        `Subject: DSN Test - Never Notify`,
 | 
			
		||||
        `Date: ${new Date().toUTCString()}`,
 | 
			
		||||
        `Message-ID: <dsn-never-${Date.now()}@example.com>`,
 | 
			
		||||
        '',
 | 
			
		||||
        'This email should not generate any DSN.',
 | 
			
		||||
        '.',
 | 
			
		||||
        ''
 | 
			
		||||
      ].join('\r\n');
 | 
			
		||||
      
 | 
			
		||||
      socket.write(email);
 | 
			
		||||
      step = 'sent';
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
 | 
			
		||||
      if (!completed) {
 | 
			
		||||
        completed = true;
 | 
			
		||||
        console.log('Email with NOTIFY=NEVER accepted');
 | 
			
		||||
        expect(true).toBeTrue();
 | 
			
		||||
        
 | 
			
		||||
        socket.write('QUIT\r\n');
 | 
			
		||||
        socket.end();
 | 
			
		||||
        done.resolve();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('error', (err) => {
 | 
			
		||||
    console.error('Socket error:', err);
 | 
			
		||||
    done.reject(err);
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  await done.promise;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('DSN - Original recipient tracking', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: 30000
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let dataBuffer = '';
 | 
			
		||||
  let step = 'greeting';
 | 
			
		||||
  let completed = false;
 | 
			
		||||
  
 | 
			
		||||
  socket.on('data', (data) => {
 | 
			
		||||
    dataBuffer += data.toString();
 | 
			
		||||
    console.log('Server response:', data.toString());
 | 
			
		||||
    
 | 
			
		||||
    if (step === 'greeting' && dataBuffer.includes('220 ')) {
 | 
			
		||||
      step = 'ehlo';
 | 
			
		||||
      socket.write('EHLO testclient\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'ehlo' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'mail';
 | 
			
		||||
      socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'mail' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'rcpt';
 | 
			
		||||
      // Include original recipient for tracking
 | 
			
		||||
      socket.write('RCPT TO:<recipient@example.com> NOTIFY=FAILURE ORCPT=rfc822;original@example.com\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'rcpt') {
 | 
			
		||||
      const accepted = dataBuffer.includes('250');
 | 
			
		||||
      const notSupported = dataBuffer.includes('501') || dataBuffer.includes('555');
 | 
			
		||||
      
 | 
			
		||||
      console.log(`ORCPT parameter: ${accepted ? 'accepted' : notSupported ? 'not supported' : 'error'}`);
 | 
			
		||||
      
 | 
			
		||||
      if (notSupported) {
 | 
			
		||||
        socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
			
		||||
        step = 'rcpt_plain';
 | 
			
		||||
        dataBuffer = '';
 | 
			
		||||
      } else if (accepted) {
 | 
			
		||||
        step = 'data';
 | 
			
		||||
        socket.write('DATA\r\n');
 | 
			
		||||
        dataBuffer = '';
 | 
			
		||||
      }
 | 
			
		||||
    } else if (step === 'rcpt_plain' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'data';
 | 
			
		||||
      socket.write('DATA\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'data' && dataBuffer.includes('354')) {
 | 
			
		||||
      const email = [
 | 
			
		||||
        `From: sender@example.com`,
 | 
			
		||||
        `To: recipient@example.com`,
 | 
			
		||||
        `Subject: DSN Test - Original Recipient`,
 | 
			
		||||
        `Date: ${new Date().toUTCString()}`,
 | 
			
		||||
        `Message-ID: <dsn-orcpt-${Date.now()}@example.com>`,
 | 
			
		||||
        '',
 | 
			
		||||
        'This email tests ORCPT parameter for tracking.',
 | 
			
		||||
        '.',
 | 
			
		||||
        ''
 | 
			
		||||
      ].join('\r\n');
 | 
			
		||||
      
 | 
			
		||||
      socket.write(email);
 | 
			
		||||
      step = 'sent';
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
 | 
			
		||||
      if (!completed) {
 | 
			
		||||
        completed = true;
 | 
			
		||||
        console.log('Email with ORCPT tracking accepted');
 | 
			
		||||
        expect(true).toBeTrue();
 | 
			
		||||
        
 | 
			
		||||
        socket.write('QUIT\r\n');
 | 
			
		||||
        socket.end();
 | 
			
		||||
        done.resolve();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('error', (err) => {
 | 
			
		||||
    console.error('Socket error:', err);
 | 
			
		||||
    done.reject(err);
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  await done.promise;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('DSN - Return parameter handling', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: 30000
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let dataBuffer = '';
 | 
			
		||||
  let step = 'greeting';
 | 
			
		||||
  
 | 
			
		||||
  socket.on('data', (data) => {
 | 
			
		||||
    dataBuffer += data.toString();
 | 
			
		||||
    console.log('Server response:', data.toString());
 | 
			
		||||
    
 | 
			
		||||
    if (step === 'greeting' && dataBuffer.includes('220 ')) {
 | 
			
		||||
      step = 'ehlo';
 | 
			
		||||
      socket.write('EHLO testclient\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'ehlo' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'mail_hdrs';
 | 
			
		||||
      // Test RET=HDRS
 | 
			
		||||
      socket.write('MAIL FROM:<sender@example.com> RET=HDRS\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'mail_hdrs') {
 | 
			
		||||
      const accepted = dataBuffer.includes('250');
 | 
			
		||||
      const notSupported = dataBuffer.includes('501') || dataBuffer.includes('555');
 | 
			
		||||
      
 | 
			
		||||
      console.log(`RET=HDRS: ${accepted ? 'accepted' : notSupported ? 'not supported' : 'error'}`);
 | 
			
		||||
      
 | 
			
		||||
      if (accepted || notSupported) {
 | 
			
		||||
        // Reset and test RET=FULL
 | 
			
		||||
        socket.write('RSET\r\n');
 | 
			
		||||
        step = 'reset';
 | 
			
		||||
        dataBuffer = '';
 | 
			
		||||
      }
 | 
			
		||||
    } else if (step === 'reset' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'mail_full';
 | 
			
		||||
      socket.write('MAIL FROM:<sender@example.com> RET=FULL\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'mail_full') {
 | 
			
		||||
      const accepted = dataBuffer.includes('250');
 | 
			
		||||
      const notSupported = dataBuffer.includes('501') || dataBuffer.includes('555');
 | 
			
		||||
      
 | 
			
		||||
      console.log(`RET=FULL: ${accepted ? 'accepted' : notSupported ? 'not supported' : 'error'}`);
 | 
			
		||||
      expect(accepted || notSupported).toBeTrue();
 | 
			
		||||
      
 | 
			
		||||
      socket.write('QUIT\r\n');
 | 
			
		||||
      socket.end();
 | 
			
		||||
      done.resolve();
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('error', (err) => {
 | 
			
		||||
    console.error('Socket error:', err);
 | 
			
		||||
    done.reject(err);
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  await done.promise;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('cleanup - stop test server', async () => {
 | 
			
		||||
  await stopTestServer();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.start();
 | 
			
		||||
							
								
								
									
										527
									
								
								test/suite/email-processing/test.email-routing.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										527
									
								
								test/suite/email-processing/test.email-routing.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,527 @@
 | 
			
		||||
import { tap, expect } from '@push.rocks/tapbundle';
 | 
			
		||||
import * as net from 'net';
 | 
			
		||||
import { startTestServer, stopTestServer } from '../server.loader.js';
 | 
			
		||||
 | 
			
		||||
const TEST_PORT = 2525;
 | 
			
		||||
 | 
			
		||||
let testServer: any;
 | 
			
		||||
 | 
			
		||||
tap.test('setup - start test server', async () => {
 | 
			
		||||
  testServer = await startTestServer();
 | 
			
		||||
  await new Promise(resolve => setTimeout(resolve, 1000));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('Email Routing - Local domain routing', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: 30000
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let dataBuffer = '';
 | 
			
		||||
  let step = 'greeting';
 | 
			
		||||
  let completed = false;
 | 
			
		||||
  
 | 
			
		||||
  socket.on('data', (data) => {
 | 
			
		||||
    if (completed) return;
 | 
			
		||||
    
 | 
			
		||||
    dataBuffer += data.toString();
 | 
			
		||||
    console.log('Server response:', data.toString());
 | 
			
		||||
    
 | 
			
		||||
    if (step === 'greeting' && dataBuffer.includes('220 ')) {
 | 
			
		||||
      step = 'ehlo';
 | 
			
		||||
      socket.write('EHLO localhost\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'ehlo' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'mail';
 | 
			
		||||
      // Local sender
 | 
			
		||||
      socket.write('MAIL FROM:<test@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'mail' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'rcpt';
 | 
			
		||||
      // Local recipient
 | 
			
		||||
      socket.write('RCPT TO:<local@localhost>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'rcpt') {
 | 
			
		||||
      const accepted = dataBuffer.includes('250');
 | 
			
		||||
      console.log(`Local domain routing: ${accepted ? 'accepted' : 'rejected'}`);
 | 
			
		||||
      
 | 
			
		||||
      if (accepted) {
 | 
			
		||||
        step = 'data';
 | 
			
		||||
        socket.write('DATA\r\n');
 | 
			
		||||
        dataBuffer = '';
 | 
			
		||||
      } else {
 | 
			
		||||
        socket.write('QUIT\r\n');
 | 
			
		||||
        socket.end();
 | 
			
		||||
        done.resolve();
 | 
			
		||||
      }
 | 
			
		||||
    } else if (step === 'data' && dataBuffer.includes('354')) {
 | 
			
		||||
      const email = [
 | 
			
		||||
        `From: test@example.com`,
 | 
			
		||||
        `To: local@localhost`,
 | 
			
		||||
        `Subject: Local Domain Routing Test`,
 | 
			
		||||
        `Date: ${new Date().toUTCString()}`,
 | 
			
		||||
        `Message-ID: <local-routing-${Date.now()}@localhost>`,
 | 
			
		||||
        '',
 | 
			
		||||
        'This email tests local domain routing.',
 | 
			
		||||
        'The server should route this email locally.',
 | 
			
		||||
        '.',
 | 
			
		||||
        ''
 | 
			
		||||
      ].join('\r\n');
 | 
			
		||||
      
 | 
			
		||||
      socket.write(email);
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
      step = 'sent';
 | 
			
		||||
    } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
 | 
			
		||||
      if (!completed) {
 | 
			
		||||
        completed = true;
 | 
			
		||||
        console.log('Local domain email routed successfully');
 | 
			
		||||
        expect(true).toBeTrue();
 | 
			
		||||
        
 | 
			
		||||
        socket.write('QUIT\r\n');
 | 
			
		||||
        socket.end();
 | 
			
		||||
        done.resolve();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('error', (err) => {
 | 
			
		||||
    console.error('Socket error:', err);
 | 
			
		||||
    done.reject(err);
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  await done.promise;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('Email Routing - External domain routing', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: 30000
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let dataBuffer = '';
 | 
			
		||||
  let step = 'greeting';
 | 
			
		||||
  let completed = false;
 | 
			
		||||
  
 | 
			
		||||
  socket.on('data', (data) => {
 | 
			
		||||
    if (completed) return;
 | 
			
		||||
    
 | 
			
		||||
    dataBuffer += data.toString();
 | 
			
		||||
    console.log('Server response:', data.toString());
 | 
			
		||||
    
 | 
			
		||||
    if (step === 'greeting' && dataBuffer.includes('220 ')) {
 | 
			
		||||
      step = 'ehlo';
 | 
			
		||||
      socket.write('EHLO localhost\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'ehlo' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'mail';
 | 
			
		||||
      socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'mail' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'rcpt';
 | 
			
		||||
      // External recipient
 | 
			
		||||
      socket.write('RCPT TO:<recipient@external.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'rcpt') {
 | 
			
		||||
      const accepted = dataBuffer.includes('250');
 | 
			
		||||
      console.log(`External domain routing: ${accepted ? 'accepted' : 'rejected'}`);
 | 
			
		||||
      
 | 
			
		||||
      if (accepted) {
 | 
			
		||||
        step = 'data';
 | 
			
		||||
        socket.write('DATA\r\n');
 | 
			
		||||
        dataBuffer = '';
 | 
			
		||||
      } else {
 | 
			
		||||
        socket.write('QUIT\r\n');
 | 
			
		||||
        socket.end();
 | 
			
		||||
        done.resolve();
 | 
			
		||||
      }
 | 
			
		||||
    } else if (step === 'data' && dataBuffer.includes('354')) {
 | 
			
		||||
      const email = [
 | 
			
		||||
        `From: sender@example.com`,
 | 
			
		||||
        `To: recipient@external.com`,
 | 
			
		||||
        `Subject: External Domain Routing Test`,
 | 
			
		||||
        `Date: ${new Date().toUTCString()}`,
 | 
			
		||||
        `Message-ID: <external-routing-${Date.now()}@example.com>`,
 | 
			
		||||
        '',
 | 
			
		||||
        'This email tests external domain routing.',
 | 
			
		||||
        'The server should accept this for relay.',
 | 
			
		||||
        '.',
 | 
			
		||||
        ''
 | 
			
		||||
      ].join('\r\n');
 | 
			
		||||
      
 | 
			
		||||
      socket.write(email);
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
      step = 'sent';
 | 
			
		||||
    } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
 | 
			
		||||
      if (!completed) {
 | 
			
		||||
        completed = true;
 | 
			
		||||
        console.log('External domain email accepted for relay');
 | 
			
		||||
        expect(true).toBeTrue();
 | 
			
		||||
        
 | 
			
		||||
        socket.write('QUIT\r\n');
 | 
			
		||||
        socket.end();
 | 
			
		||||
        done.resolve();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('error', (err) => {
 | 
			
		||||
    console.error('Socket error:', err);
 | 
			
		||||
    done.reject(err);
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  await done.promise;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('Email Routing - Multiple recipients', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: 30000
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let dataBuffer = '';
 | 
			
		||||
  let step = 'greeting';
 | 
			
		||||
  let recipientCount = 0;
 | 
			
		||||
  const totalRecipients = 5;
 | 
			
		||||
  let completed = false;
 | 
			
		||||
  
 | 
			
		||||
  socket.on('data', (data) => {
 | 
			
		||||
    if (completed) return;
 | 
			
		||||
    
 | 
			
		||||
    dataBuffer += data.toString();
 | 
			
		||||
    console.log('Server response:', data.toString());
 | 
			
		||||
    
 | 
			
		||||
    if (step === 'greeting' && dataBuffer.includes('220 ')) {
 | 
			
		||||
      step = 'ehlo';
 | 
			
		||||
      socket.write('EHLO localhost\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'ehlo' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'mail';
 | 
			
		||||
      socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'mail' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'rcpt';
 | 
			
		||||
      recipientCount++;
 | 
			
		||||
      socket.write(`RCPT TO:<recipient${recipientCount}@example.com>\r\n`);
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'rcpt' && dataBuffer.includes('250')) {
 | 
			
		||||
      if (recipientCount < totalRecipients) {
 | 
			
		||||
        recipientCount++;
 | 
			
		||||
        socket.write(`RCPT TO:<recipient${recipientCount}@example.com>\r\n`);
 | 
			
		||||
        dataBuffer = '';
 | 
			
		||||
      } else {
 | 
			
		||||
        console.log(`All ${totalRecipients} recipients accepted`);
 | 
			
		||||
        step = 'data';
 | 
			
		||||
        socket.write('DATA\r\n');
 | 
			
		||||
        dataBuffer = '';
 | 
			
		||||
      }
 | 
			
		||||
    } else if (step === 'data' && dataBuffer.includes('354')) {
 | 
			
		||||
      const recipients = Array.from({length: totalRecipients}, (_, i) => `recipient${i+1}@example.com`);
 | 
			
		||||
      const email = [
 | 
			
		||||
        `From: sender@example.com`,
 | 
			
		||||
        `To: ${recipients.join(', ')}`,
 | 
			
		||||
        `Subject: Multiple Recipients Routing Test`,
 | 
			
		||||
        `Date: ${new Date().toUTCString()}`,
 | 
			
		||||
        `Message-ID: <multi-recipient-${Date.now()}@example.com>`,
 | 
			
		||||
        '',
 | 
			
		||||
        'This email tests routing to multiple recipients.',
 | 
			
		||||
        `Total recipients: ${totalRecipients}`,
 | 
			
		||||
        '.',
 | 
			
		||||
        ''
 | 
			
		||||
      ].join('\r\n');
 | 
			
		||||
      
 | 
			
		||||
      socket.write(email);
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
      step = 'sent';
 | 
			
		||||
    } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
 | 
			
		||||
      if (!completed) {
 | 
			
		||||
        completed = true;
 | 
			
		||||
        console.log('Email with multiple recipients routed successfully');
 | 
			
		||||
        expect(true).toBeTrue();
 | 
			
		||||
        
 | 
			
		||||
        socket.write('QUIT\r\n');
 | 
			
		||||
        socket.end();
 | 
			
		||||
        done.resolve();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('error', (err) => {
 | 
			
		||||
    console.error('Socket error:', err);
 | 
			
		||||
    done.reject(err);
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  await done.promise;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('Email Routing - Invalid domain handling', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: 30000
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let dataBuffer = '';
 | 
			
		||||
  let step = 'greeting';
 | 
			
		||||
  let testType = 'invalid-tld';
 | 
			
		||||
  const testCases = [
 | 
			
		||||
    { email: 'user@invalid-tld', type: 'invalid-tld' },
 | 
			
		||||
    { email: 'user@.com', type: 'missing-domain' },
 | 
			
		||||
    { email: 'user@domain..com', type: 'double-dot' },
 | 
			
		||||
    { email: 'user@-domain.com', type: 'leading-dash' },
 | 
			
		||||
    { email: 'user@domain-.com', type: 'trailing-dash' }
 | 
			
		||||
  ];
 | 
			
		||||
  let currentTest = 0;
 | 
			
		||||
  
 | 
			
		||||
  socket.on('data', (data) => {
 | 
			
		||||
    dataBuffer += data.toString();
 | 
			
		||||
    console.log('Server response:', data.toString());
 | 
			
		||||
    
 | 
			
		||||
    if (step === 'greeting' && dataBuffer.includes('220 ')) {
 | 
			
		||||
      step = 'ehlo';
 | 
			
		||||
      socket.write('EHLO localhost\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'ehlo' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'mail';
 | 
			
		||||
      socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'mail' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'rcpt';
 | 
			
		||||
      testType = testCases[currentTest].type;
 | 
			
		||||
      socket.write(`RCPT TO:<${testCases[currentTest].email}>\r\n`);
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'rcpt') {
 | 
			
		||||
      const rejected = dataBuffer.includes('550') || dataBuffer.includes('553') || dataBuffer.includes('501');
 | 
			
		||||
      console.log(`Invalid domain test (${testType}): ${rejected ? 'properly rejected' : 'unexpectedly accepted'}`);
 | 
			
		||||
      
 | 
			
		||||
      currentTest++;
 | 
			
		||||
      if (currentTest < testCases.length) {
 | 
			
		||||
        // Reset for next test
 | 
			
		||||
        socket.write('RSET\r\n');
 | 
			
		||||
        step = 'rset';
 | 
			
		||||
        dataBuffer = '';
 | 
			
		||||
      } else {
 | 
			
		||||
        socket.write('QUIT\r\n');
 | 
			
		||||
        socket.end();
 | 
			
		||||
        done.resolve();
 | 
			
		||||
      }
 | 
			
		||||
    } else if (step === 'rset' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'mail';
 | 
			
		||||
      socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('error', (err) => {
 | 
			
		||||
    console.error('Socket error:', err);
 | 
			
		||||
    done.reject(err);
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  await done.promise;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('Email Routing - Mixed local and external recipients', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: 30000
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let dataBuffer = '';
 | 
			
		||||
  let step = 'greeting';
 | 
			
		||||
  let completed = false;
 | 
			
		||||
  const recipients = [
 | 
			
		||||
    'local@localhost',
 | 
			
		||||
    'external@example.com',
 | 
			
		||||
    'another@localhost',
 | 
			
		||||
    'remote@external.com'
 | 
			
		||||
  ];
 | 
			
		||||
  let currentRecipient = 0;
 | 
			
		||||
  let acceptedRecipients: string[] = [];
 | 
			
		||||
  
 | 
			
		||||
  socket.on('data', (data) => {
 | 
			
		||||
    if (completed) return;
 | 
			
		||||
    
 | 
			
		||||
    dataBuffer += data.toString();
 | 
			
		||||
    console.log('Server response:', data.toString());
 | 
			
		||||
    
 | 
			
		||||
    if (step === 'greeting' && dataBuffer.includes('220 ')) {
 | 
			
		||||
      step = 'ehlo';
 | 
			
		||||
      socket.write('EHLO localhost\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'ehlo' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'mail';
 | 
			
		||||
      socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'mail' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'rcpt';
 | 
			
		||||
      socket.write(`RCPT TO:<${recipients[currentRecipient]}>\r\n`);
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'rcpt') {
 | 
			
		||||
      if (dataBuffer.includes('250')) {
 | 
			
		||||
        acceptedRecipients.push(recipients[currentRecipient]);
 | 
			
		||||
        console.log(`Recipient ${recipients[currentRecipient]} accepted`);
 | 
			
		||||
      } else {
 | 
			
		||||
        console.log(`Recipient ${recipients[currentRecipient]} rejected`);
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      currentRecipient++;
 | 
			
		||||
      if (currentRecipient < recipients.length) {
 | 
			
		||||
        socket.write(`RCPT TO:<${recipients[currentRecipient]}>\r\n`);
 | 
			
		||||
        dataBuffer = '';
 | 
			
		||||
      } else if (acceptedRecipients.length > 0) {
 | 
			
		||||
        step = 'data';
 | 
			
		||||
        socket.write('DATA\r\n');
 | 
			
		||||
        dataBuffer = '';
 | 
			
		||||
      } else {
 | 
			
		||||
        socket.write('QUIT\r\n');
 | 
			
		||||
        socket.end();
 | 
			
		||||
        done.resolve();
 | 
			
		||||
      }
 | 
			
		||||
    } else if (step === 'data' && dataBuffer.includes('354')) {
 | 
			
		||||
      const email = [
 | 
			
		||||
        `From: sender@example.com`,
 | 
			
		||||
        `To: ${acceptedRecipients.join(', ')}`,
 | 
			
		||||
        `Subject: Mixed Recipients Routing Test`,
 | 
			
		||||
        `Date: ${new Date().toUTCString()}`,
 | 
			
		||||
        `Message-ID: <mixed-routing-${Date.now()}@example.com>`,
 | 
			
		||||
        '',
 | 
			
		||||
        'This email tests routing to mixed local and external recipients.',
 | 
			
		||||
        `Accepted recipients: ${acceptedRecipients.length}`,
 | 
			
		||||
        '.',
 | 
			
		||||
        ''
 | 
			
		||||
      ].join('\r\n');
 | 
			
		||||
      
 | 
			
		||||
      socket.write(email);
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
      step = 'sent';
 | 
			
		||||
    } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
 | 
			
		||||
      if (!completed) {
 | 
			
		||||
        completed = true;
 | 
			
		||||
        console.log('Email with mixed recipients routed successfully');
 | 
			
		||||
        expect(acceptedRecipients.length).toBeGreaterThan(0);
 | 
			
		||||
        
 | 
			
		||||
        socket.write('QUIT\r\n');
 | 
			
		||||
        socket.end();
 | 
			
		||||
        done.resolve();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('error', (err) => {
 | 
			
		||||
    console.error('Socket error:', err);
 | 
			
		||||
    done.reject(err);
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  await done.promise;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('Email Routing - Subdomain routing', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: 30000
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let dataBuffer = '';
 | 
			
		||||
  let step = 'greeting';
 | 
			
		||||
  let completed = false;
 | 
			
		||||
  const subdomainTests = [
 | 
			
		||||
    'user@mail.example.com',
 | 
			
		||||
    'user@smtp.corp.example.com',
 | 
			
		||||
    'user@deep.sub.domain.example.com'
 | 
			
		||||
  ];
 | 
			
		||||
  let currentTest = 0;
 | 
			
		||||
  
 | 
			
		||||
  socket.on('data', (data) => {
 | 
			
		||||
    if (completed) return;
 | 
			
		||||
    
 | 
			
		||||
    dataBuffer += data.toString();
 | 
			
		||||
    console.log('Server response:', data.toString());
 | 
			
		||||
    
 | 
			
		||||
    if (step === 'greeting' && dataBuffer.includes('220 ')) {
 | 
			
		||||
      step = 'ehlo';
 | 
			
		||||
      socket.write('EHLO localhost\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'ehlo' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'mail';
 | 
			
		||||
      socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'mail' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'rcpt';
 | 
			
		||||
      socket.write(`RCPT TO:<${subdomainTests[currentTest]}>\r\n`);
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'rcpt') {
 | 
			
		||||
      const accepted = dataBuffer.includes('250');
 | 
			
		||||
      console.log(`Subdomain routing test (${subdomainTests[currentTest]}): ${accepted ? 'accepted' : 'rejected'}`);
 | 
			
		||||
      
 | 
			
		||||
      currentTest++;
 | 
			
		||||
      if (currentTest < subdomainTests.length) {
 | 
			
		||||
        socket.write('RSET\r\n');
 | 
			
		||||
        step = 'rset';
 | 
			
		||||
        dataBuffer = '';
 | 
			
		||||
      } else {
 | 
			
		||||
        step = 'data';
 | 
			
		||||
        socket.write('DATA\r\n');
 | 
			
		||||
        dataBuffer = '';
 | 
			
		||||
      }
 | 
			
		||||
    } else if (step === 'rset' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'mail';
 | 
			
		||||
      socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'data' && dataBuffer.includes('354')) {
 | 
			
		||||
      const email = [
 | 
			
		||||
        `From: sender@example.com`,
 | 
			
		||||
        `To: ${subdomainTests[subdomainTests.length - 1]}`,
 | 
			
		||||
        `Subject: Subdomain Routing Test`,
 | 
			
		||||
        `Date: ${new Date().toUTCString()}`,
 | 
			
		||||
        `Message-ID: <subdomain-routing-${Date.now()}@example.com>`,
 | 
			
		||||
        '',
 | 
			
		||||
        'This email tests subdomain routing.',
 | 
			
		||||
        '.',
 | 
			
		||||
        ''
 | 
			
		||||
      ].join('\r\n');
 | 
			
		||||
      
 | 
			
		||||
      socket.write(email);
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
      step = 'sent';
 | 
			
		||||
    } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
 | 
			
		||||
      if (!completed) {
 | 
			
		||||
        completed = true;
 | 
			
		||||
        console.log('Subdomain routing test completed');
 | 
			
		||||
        expect(true).toBeTrue();
 | 
			
		||||
        
 | 
			
		||||
        socket.write('QUIT\r\n');
 | 
			
		||||
        socket.end();
 | 
			
		||||
        done.resolve();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('error', (err) => {
 | 
			
		||||
    console.error('Socket error:', err);
 | 
			
		||||
    done.reject(err);
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  await done.promise;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('cleanup - stop test server', async () => {
 | 
			
		||||
  await stopTestServer();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.start();
 | 
			
		||||
							
								
								
									
										315
									
								
								test/suite/email-processing/test.invalid-email-addresses.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										315
									
								
								test/suite/email-processing/test.invalid-email-addresses.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,315 @@
 | 
			
		||||
import { tap, expect } from '@push.rocks/tapbundle';
 | 
			
		||||
import * as net from 'net';
 | 
			
		||||
import { startTestServer, stopTestServer } from '../server.loader.js';
 | 
			
		||||
 | 
			
		||||
// Test configuration
 | 
			
		||||
const TEST_PORT = 2525;
 | 
			
		||||
const TEST_TIMEOUT = 20000;
 | 
			
		||||
 | 
			
		||||
let testServer: any;
 | 
			
		||||
 | 
			
		||||
// Setup
 | 
			
		||||
tap.test('setup - start SMTP server', async () => {
 | 
			
		||||
  testServer = await startTestServer();
 | 
			
		||||
  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();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Start the test
 | 
			
		||||
tap.start();
 | 
			
		||||
							
								
								
									
										506
									
								
								test/suite/email-processing/test.large-email.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										506
									
								
								test/suite/email-processing/test.large-email.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,506 @@
 | 
			
		||||
import { tap, expect } from '@push.rocks/tapbundle';
 | 
			
		||||
import * as net from 'net';
 | 
			
		||||
import * as path from 'path';
 | 
			
		||||
import { startTestServer, stopTestServer } from '../server.loader.js';
 | 
			
		||||
 | 
			
		||||
// Test configuration
 | 
			
		||||
const TEST_PORT = 2525;
 | 
			
		||||
const TEST_TIMEOUT = 60000; // Increased for large email handling
 | 
			
		||||
 | 
			
		||||
let testServer: any;
 | 
			
		||||
 | 
			
		||||
// Setup
 | 
			
		||||
tap.test('setup - start SMTP server', async () => {
 | 
			
		||||
  testServer = await startTestServer();
 | 
			
		||||
  await new Promise(resolve => setTimeout(resolve, 1000));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Test: Moderately large email (1MB)
 | 
			
		||||
tap.test('Large Email - should handle 1MB email', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: TEST_TIMEOUT
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let receivedData = '';
 | 
			
		||||
  let currentStep = 'connecting';
 | 
			
		||||
  let completed = false;
 | 
			
		||||
  
 | 
			
		||||
  // Generate 1MB of content
 | 
			
		||||
  const largeBody = 'X'.repeat(1024 * 1024); // 1MB
 | 
			
		||||
  const emailContent = `Subject: 1MB Email Test\r\nFrom: sender@example.com\r\nTo: recipient@example.com\r\n\r\n${largeBody}\r\n`;
 | 
			
		||||
  
 | 
			
		||||
  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:<sender@example.com>\r\n');
 | 
			
		||||
    } else if (currentStep === 'mail_from' && receivedData.includes('250')) {
 | 
			
		||||
      currentStep = 'rcpt_to';
 | 
			
		||||
      socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
			
		||||
    } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) {
 | 
			
		||||
      currentStep = 'data';
 | 
			
		||||
      socket.write('DATA\r\n');
 | 
			
		||||
    } else if (currentStep === 'data' && receivedData.includes('354')) {
 | 
			
		||||
      currentStep = 'sending_large_email';
 | 
			
		||||
      
 | 
			
		||||
      // Send in chunks to avoid overwhelming
 | 
			
		||||
      const chunkSize = 64 * 1024; // 64KB chunks
 | 
			
		||||
      let sent = 0;
 | 
			
		||||
      
 | 
			
		||||
      const sendChunk = () => {
 | 
			
		||||
        if (sent < emailContent.length) {
 | 
			
		||||
          const chunk = emailContent.slice(sent, sent + chunkSize);
 | 
			
		||||
          socket.write(chunk);
 | 
			
		||||
          sent += chunk.length;
 | 
			
		||||
          
 | 
			
		||||
          // Small delay between chunks
 | 
			
		||||
          if (sent < emailContent.length) {
 | 
			
		||||
            setTimeout(sendChunk, 10);
 | 
			
		||||
          } else {
 | 
			
		||||
            // End of data
 | 
			
		||||
            socket.write('.\r\n');
 | 
			
		||||
            currentStep = 'sent';
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
      
 | 
			
		||||
      sendChunk();
 | 
			
		||||
    } else if (currentStep === 'sent' && (receivedData.includes('250') || receivedData.includes('552'))) {
 | 
			
		||||
      if (!completed) {
 | 
			
		||||
        completed = true;
 | 
			
		||||
        socket.write('QUIT\r\n');
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          socket.destroy();
 | 
			
		||||
          // Either accepted (250) or size exceeded (552)
 | 
			
		||||
          expect(receivedData).toMatch(/250|552/);
 | 
			
		||||
          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;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Test: Large email with MIME attachments
 | 
			
		||||
tap.test('Large Email - should handle multi-part MIME message', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: TEST_TIMEOUT
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let receivedData = '';
 | 
			
		||||
  let currentStep = 'connecting';
 | 
			
		||||
  let completed = false;
 | 
			
		||||
  
 | 
			
		||||
  const boundary = '----=_Part_0_123456789';
 | 
			
		||||
  const attachment1 = 'A'.repeat(500 * 1024); // 500KB
 | 
			
		||||
  const attachment2 = 'B'.repeat(300 * 1024); // 300KB
 | 
			
		||||
  
 | 
			
		||||
  const emailContent = [
 | 
			
		||||
    'Subject: Large MIME Email Test',
 | 
			
		||||
    'From: sender@example.com',
 | 
			
		||||
    'To: recipient@example.com',
 | 
			
		||||
    'MIME-Version: 1.0',
 | 
			
		||||
    `Content-Type: multipart/mixed; boundary="${boundary}"`,
 | 
			
		||||
    '',
 | 
			
		||||
    'This is a multi-part message in MIME format.',
 | 
			
		||||
    '',
 | 
			
		||||
    `--${boundary}`,
 | 
			
		||||
    'Content-Type: text/plain; charset=utf-8',
 | 
			
		||||
    '',
 | 
			
		||||
    'This email contains large attachments.',
 | 
			
		||||
    '',
 | 
			
		||||
    `--${boundary}`,
 | 
			
		||||
    'Content-Type: text/plain; charset=utf-8',
 | 
			
		||||
    'Content-Disposition: attachment; filename="file1.txt"',
 | 
			
		||||
    '',
 | 
			
		||||
    attachment1,
 | 
			
		||||
    '',
 | 
			
		||||
    `--${boundary}`,
 | 
			
		||||
    'Content-Type: application/octet-stream',
 | 
			
		||||
    'Content-Disposition: attachment; filename="file2.bin"',
 | 
			
		||||
    'Content-Transfer-Encoding: base64',
 | 
			
		||||
    '',
 | 
			
		||||
    Buffer.from(attachment2).toString('base64'),
 | 
			
		||||
    '',
 | 
			
		||||
    `--${boundary}--`,
 | 
			
		||||
    ''
 | 
			
		||||
  ].join('\r\n');
 | 
			
		||||
  
 | 
			
		||||
  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:<sender@example.com>\r\n');
 | 
			
		||||
    } else if (currentStep === 'mail_from' && receivedData.includes('250')) {
 | 
			
		||||
      currentStep = 'rcpt_to';
 | 
			
		||||
      socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
			
		||||
    } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) {
 | 
			
		||||
      currentStep = 'data';
 | 
			
		||||
      socket.write('DATA\r\n');
 | 
			
		||||
    } else if (currentStep === 'data' && receivedData.includes('354')) {
 | 
			
		||||
      currentStep = 'sending_mime';
 | 
			
		||||
      socket.write(emailContent);
 | 
			
		||||
      socket.write('\r\n.\r\n');
 | 
			
		||||
      currentStep = 'sent';
 | 
			
		||||
    } else if (currentStep === 'sent' && (receivedData.includes('250') || receivedData.includes('552'))) {
 | 
			
		||||
      if (!completed) {
 | 
			
		||||
        completed = true;
 | 
			
		||||
        socket.write('QUIT\r\n');
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          socket.destroy();
 | 
			
		||||
          expect(receivedData).toMatch(/250|552/);
 | 
			
		||||
          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;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Test: Email size limits with SIZE extension
 | 
			
		||||
tap.test('Large Email - should respect SIZE limits if advertised', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: TEST_TIMEOUT
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let receivedData = '';
 | 
			
		||||
  let currentStep = 'connecting';
 | 
			
		||||
  let maxSize: number | null = null;
 | 
			
		||||
  
 | 
			
		||||
  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')) {
 | 
			
		||||
      // Check for SIZE extension
 | 
			
		||||
      const sizeMatch = receivedData.match(/SIZE\s+(\d+)/);
 | 
			
		||||
      if (sizeMatch) {
 | 
			
		||||
        maxSize = parseInt(sizeMatch[1]);
 | 
			
		||||
        console.log(`Server advertises max size: ${maxSize} bytes`);
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      currentStep = 'mail_from';
 | 
			
		||||
      const emailSize = maxSize ? maxSize + 1000 : 5000000; // Over limit or 5MB
 | 
			
		||||
      socket.write(`MAIL FROM:<sender@example.com> SIZE=${emailSize}\r\n`);
 | 
			
		||||
    } else if (currentStep === 'mail_from') {
 | 
			
		||||
      if (maxSize && receivedData.includes('552')) {
 | 
			
		||||
        // Size rejected - expected
 | 
			
		||||
        socket.write('QUIT\r\n');
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          socket.destroy();
 | 
			
		||||
          expect(receivedData).toInclude('552');
 | 
			
		||||
          done.resolve();
 | 
			
		||||
        }, 100);
 | 
			
		||||
      } else if (receivedData.includes('250')) {
 | 
			
		||||
        // Size accepted or no limit
 | 
			
		||||
        socket.write('QUIT\r\n');
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          socket.destroy();
 | 
			
		||||
          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;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Test: Very large email handling (5MB)
 | 
			
		||||
tap.test('Large Email - should handle or reject very large emails gracefully', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: TEST_TIMEOUT
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let receivedData = '';
 | 
			
		||||
  let currentStep = 'connecting';
 | 
			
		||||
  let completed = false;
 | 
			
		||||
  
 | 
			
		||||
  // Generate 5MB email
 | 
			
		||||
  const largeContent = 'X'.repeat(5 * 1024 * 1024); // 5MB
 | 
			
		||||
  const emailContent = `Subject: 5MB Email Test\r\nFrom: sender@example.com\r\nTo: recipient@example.com\r\n\r\n${largeContent}\r\n`;
 | 
			
		||||
  
 | 
			
		||||
  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:<sender@example.com>\r\n');
 | 
			
		||||
    } else if (currentStep === 'mail_from' && receivedData.includes('250')) {
 | 
			
		||||
      currentStep = 'rcpt_to';
 | 
			
		||||
      socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
			
		||||
    } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) {
 | 
			
		||||
      currentStep = 'data';
 | 
			
		||||
      socket.write('DATA\r\n');
 | 
			
		||||
    } else if (currentStep === 'data' && receivedData.includes('354')) {
 | 
			
		||||
      currentStep = 'sending_5mb';
 | 
			
		||||
      
 | 
			
		||||
      console.log('Sending 5MB email...');
 | 
			
		||||
      
 | 
			
		||||
      // Send in larger chunks for efficiency
 | 
			
		||||
      const chunkSize = 256 * 1024; // 256KB chunks
 | 
			
		||||
      let sent = 0;
 | 
			
		||||
      
 | 
			
		||||
      const sendChunk = () => {
 | 
			
		||||
        if (sent < emailContent.length) {
 | 
			
		||||
          const chunk = emailContent.slice(sent, sent + chunkSize);
 | 
			
		||||
          socket.write(chunk);
 | 
			
		||||
          sent += chunk.length;
 | 
			
		||||
          
 | 
			
		||||
          if (sent < emailContent.length) {
 | 
			
		||||
            setImmediate(sendChunk); // Use setImmediate for better performance
 | 
			
		||||
          } else {
 | 
			
		||||
            socket.write('.\r\n');
 | 
			
		||||
            currentStep = 'sent';
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
      
 | 
			
		||||
      sendChunk();
 | 
			
		||||
    } else if (currentStep === 'sent') {
 | 
			
		||||
      const responseCode = receivedData.match(/(\d{3})/)?.[1];
 | 
			
		||||
      if (responseCode && !completed) {
 | 
			
		||||
        completed = true;
 | 
			
		||||
        socket.write('QUIT\r\n');
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          socket.destroy();
 | 
			
		||||
          // Accept various responses: 250 (accepted), 552 (size exceeded), 554 (failed)
 | 
			
		||||
          expect(responseCode).toMatch(/^(250|552|554|451|452)$/);
 | 
			
		||||
          done.resolve();
 | 
			
		||||
        }, 100);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('error', (error) => {
 | 
			
		||||
    // Connection errors during large transfers are acceptable
 | 
			
		||||
    if (currentStep === 'sending_5mb' || currentStep === 'sent') {
 | 
			
		||||
      done.resolve();
 | 
			
		||||
    } else {
 | 
			
		||||
      done.reject(error);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('timeout', () => {
 | 
			
		||||
    socket.destroy();
 | 
			
		||||
    done.reject(new Error(`Connection timeout at step: ${currentStep}`));
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  await done.promise;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Test: Chunked transfer handling
 | 
			
		||||
tap.test('Large Email - should handle chunked transfers properly', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: TEST_TIMEOUT
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let receivedData = '';
 | 
			
		||||
  let currentStep = 'connecting';
 | 
			
		||||
  let chunksSent = 0;
 | 
			
		||||
  let completed = false;
 | 
			
		||||
  
 | 
			
		||||
  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:<sender@example.com>\r\n');
 | 
			
		||||
    } else if (currentStep === 'mail_from' && receivedData.includes('250')) {
 | 
			
		||||
      currentStep = 'rcpt_to';
 | 
			
		||||
      socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
			
		||||
    } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) {
 | 
			
		||||
      currentStep = 'data';
 | 
			
		||||
      socket.write('DATA\r\n');
 | 
			
		||||
    } else if (currentStep === 'data' && receivedData.includes('354')) {
 | 
			
		||||
      currentStep = 'chunked_sending';
 | 
			
		||||
      
 | 
			
		||||
      // Send headers
 | 
			
		||||
      socket.write('Subject: Chunked Transfer Test\r\n');
 | 
			
		||||
      socket.write('From: sender@example.com\r\n');
 | 
			
		||||
      socket.write('To: recipient@example.com\r\n');
 | 
			
		||||
      socket.write('\r\n');
 | 
			
		||||
      
 | 
			
		||||
      // Send body in multiple chunks with delays
 | 
			
		||||
      const chunks = [
 | 
			
		||||
        'First chunk of data\r\n',
 | 
			
		||||
        'Second chunk of data\r\n',
 | 
			
		||||
        'Third chunk of data\r\n',
 | 
			
		||||
        'Fourth chunk of data\r\n',
 | 
			
		||||
        'Final chunk of data\r\n'
 | 
			
		||||
      ];
 | 
			
		||||
      
 | 
			
		||||
      const sendNextChunk = () => {
 | 
			
		||||
        if (chunksSent < chunks.length) {
 | 
			
		||||
          socket.write(chunks[chunksSent]);
 | 
			
		||||
          chunksSent++;
 | 
			
		||||
          setTimeout(sendNextChunk, 100); // 100ms delay between chunks
 | 
			
		||||
        } else {
 | 
			
		||||
          socket.write('.\r\n');
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
      
 | 
			
		||||
      sendNextChunk();
 | 
			
		||||
    } else if (currentStep === 'chunked_sending' && receivedData.includes('250')) {
 | 
			
		||||
      if (!completed) {
 | 
			
		||||
        completed = true;
 | 
			
		||||
        socket.write('QUIT\r\n');
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          socket.destroy();
 | 
			
		||||
          expect(chunksSent).toEqual(5);
 | 
			
		||||
          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;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Test: Email with very long lines
 | 
			
		||||
tap.test('Large Email - should handle emails with very long lines', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: TEST_TIMEOUT
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let receivedData = '';
 | 
			
		||||
  let currentStep = 'connecting';
 | 
			
		||||
  let completed = false;
 | 
			
		||||
  
 | 
			
		||||
  // Create a very long line (10KB)
 | 
			
		||||
  const veryLongLine = 'A'.repeat(10 * 1024);
 | 
			
		||||
  const emailContent = `Subject: Long Line Test\r\nFrom: sender@example.com\r\nTo: recipient@example.com\r\n\r\n${veryLongLine}\r\nNormal line after long line.\r\n`;
 | 
			
		||||
  
 | 
			
		||||
  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:<sender@example.com>\r\n');
 | 
			
		||||
    } else if (currentStep === 'mail_from' && receivedData.includes('250')) {
 | 
			
		||||
      currentStep = 'rcpt_to';
 | 
			
		||||
      socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
			
		||||
    } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) {
 | 
			
		||||
      currentStep = 'data';
 | 
			
		||||
      socket.write('DATA\r\n');
 | 
			
		||||
    } else if (currentStep === 'data' && receivedData.includes('354')) {
 | 
			
		||||
      currentStep = 'long_line';
 | 
			
		||||
      socket.write(emailContent);
 | 
			
		||||
      socket.write('.\r\n');
 | 
			
		||||
      currentStep = 'sent';
 | 
			
		||||
    } else if (currentStep === 'sent') {
 | 
			
		||||
      const responseCode = receivedData.match(/(\d{3})/)?.[1];
 | 
			
		||||
      if (responseCode && !completed) {
 | 
			
		||||
        completed = true;
 | 
			
		||||
        socket.write('QUIT\r\n');
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          socket.destroy();
 | 
			
		||||
          // May accept or reject based on line length limits
 | 
			
		||||
          expect(responseCode).toMatch(/^(250|500|501|552)$/);
 | 
			
		||||
          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 () => {
 | 
			
		||||
  if (testServer) {
 | 
			
		||||
    await stopTestServer();
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Start the test
 | 
			
		||||
tap.start();
 | 
			
		||||
							
								
								
									
										513
									
								
								test/suite/email-processing/test.mime-handling.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										513
									
								
								test/suite/email-processing/test.mime-handling.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,513 @@
 | 
			
		||||
import { tap, expect } from '@push.rocks/tapbundle';
 | 
			
		||||
import * as net from 'net';
 | 
			
		||||
import { startTestServer, stopTestServer, TEST_PORT, sendEmailWithRawSocket } from '../server.loader.js';
 | 
			
		||||
 | 
			
		||||
let testServer: any;
 | 
			
		||||
 | 
			
		||||
tap.test('setup - start test server', async () => {
 | 
			
		||||
  testServer = await startTestServer();
 | 
			
		||||
  await new Promise(resolve => setTimeout(resolve, 1000));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('MIME Handling - Comprehensive multipart message', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: 30000
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let dataBuffer = '';
 | 
			
		||||
  let step = 'greeting';
 | 
			
		||||
  let completed = false;
 | 
			
		||||
  
 | 
			
		||||
  socket.on('data', (data) => {
 | 
			
		||||
    dataBuffer += data.toString();
 | 
			
		||||
    console.log('Server response:', data.toString());
 | 
			
		||||
    
 | 
			
		||||
    if (step === 'greeting' && dataBuffer.includes('220 ')) {
 | 
			
		||||
      step = 'ehlo';
 | 
			
		||||
      socket.write('EHLO testclient\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'ehlo' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'mail';
 | 
			
		||||
      socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'mail' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'rcpt';
 | 
			
		||||
      socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'rcpt' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'data';
 | 
			
		||||
      socket.write('DATA\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'data' && dataBuffer.includes('354')) {
 | 
			
		||||
      // Create comprehensive MIME test email
 | 
			
		||||
      const boundary = 'mime-test-boundary-12345';
 | 
			
		||||
      const innerBoundary = 'inner-mime-boundary-67890';
 | 
			
		||||
      
 | 
			
		||||
      const email = [
 | 
			
		||||
        `From: sender@example.com`,
 | 
			
		||||
        `To: recipient@example.com`,
 | 
			
		||||
        `Subject: MIME Handling Test - Comprehensive`,
 | 
			
		||||
        `Date: ${new Date().toUTCString()}`,
 | 
			
		||||
        `Message-ID: <mime-test-${Date.now()}@example.com>`,
 | 
			
		||||
        `MIME-Version: 1.0`,
 | 
			
		||||
        `Content-Type: multipart/mixed; boundary="${boundary}"`,
 | 
			
		||||
        '',
 | 
			
		||||
        'This is a multi-part message in MIME format.',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: text/plain; charset=utf-8`,
 | 
			
		||||
        `Content-Transfer-Encoding: 7bit`,
 | 
			
		||||
        '',
 | 
			
		||||
        'This is the plain text part of the email.',
 | 
			
		||||
        'It tests basic MIME text handling.',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: text/html; charset=utf-8`,
 | 
			
		||||
        `Content-Transfer-Encoding: quoted-printable`,
 | 
			
		||||
        '',
 | 
			
		||||
        '<html>',
 | 
			
		||||
        '<head><title>MIME Test</title></head>',
 | 
			
		||||
        '<body>',
 | 
			
		||||
        '<h1>HTML MIME Content</h1>',
 | 
			
		||||
        '<p>This tests HTML MIME content handling.</p>',
 | 
			
		||||
        '<p>Special chars: =E2=98=85 =E2=9C=93 =E2=9D=A4</p>',
 | 
			
		||||
        '</body>',
 | 
			
		||||
        '</html>',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: multipart/alternative; boundary="${innerBoundary}"`,
 | 
			
		||||
        '',
 | 
			
		||||
        `--${innerBoundary}`,
 | 
			
		||||
        `Content-Type: text/plain; charset=iso-8859-1`,
 | 
			
		||||
        `Content-Transfer-Encoding: base64`,
 | 
			
		||||
        '',
 | 
			
		||||
        'VGhpcyBpcyBiYXNlNjQgZW5jb2RlZCB0ZXh0IGNvbnRlbnQu',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${innerBoundary}`,
 | 
			
		||||
        `Content-Type: application/json; charset=utf-8`,
 | 
			
		||||
        '',
 | 
			
		||||
        '{"message": "JSON MIME content", "test": true, "special": "àáâãäå"}',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${innerBoundary}--`,
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: image/png`,
 | 
			
		||||
        `Content-Disposition: attachment; filename="test.png"`,
 | 
			
		||||
        `Content-Transfer-Encoding: base64`,
 | 
			
		||||
        '',
 | 
			
		||||
        'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: text/csv`,
 | 
			
		||||
        `Content-Disposition: attachment; filename="data.csv"`,
 | 
			
		||||
        '',
 | 
			
		||||
        'Name,Age,Email',
 | 
			
		||||
        'John,25,john@example.com',
 | 
			
		||||
        'Jane,30,jane@example.com',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: application/pdf`,
 | 
			
		||||
        `Content-Disposition: attachment; filename="document.pdf"`,
 | 
			
		||||
        `Content-Transfer-Encoding: base64`,
 | 
			
		||||
        '',
 | 
			
		||||
        'JVBERi0xLjQKJcOkw7zDtsOVDQo=',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}--`,
 | 
			
		||||
        '.',
 | 
			
		||||
        ''
 | 
			
		||||
      ].join('\r\n');
 | 
			
		||||
      
 | 
			
		||||
      console.log('Sending comprehensive MIME email with multiple parts and encodings');
 | 
			
		||||
      socket.write(email);
 | 
			
		||||
      step = 'sent';
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
 | 
			
		||||
      if (!completed) {
 | 
			
		||||
        completed = true;
 | 
			
		||||
        console.log('Complex MIME message accepted successfully');
 | 
			
		||||
        expect(true).toBeTrue();
 | 
			
		||||
        
 | 
			
		||||
        socket.write('QUIT\r\n');
 | 
			
		||||
        socket.end();
 | 
			
		||||
        done.resolve();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('error', (err) => {
 | 
			
		||||
    console.error('Socket error:', err);
 | 
			
		||||
    done.reject(err);
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  await done.promise;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('MIME Handling - Quoted-printable encoding', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: 30000
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let dataBuffer = '';
 | 
			
		||||
  let step = 'greeting';
 | 
			
		||||
  let completed = false;
 | 
			
		||||
  
 | 
			
		||||
  socket.on('data', (data) => {
 | 
			
		||||
    dataBuffer += data.toString();
 | 
			
		||||
    console.log('Server response:', data.toString());
 | 
			
		||||
    
 | 
			
		||||
    if (step === 'greeting' && dataBuffer.includes('220 ')) {
 | 
			
		||||
      step = 'ehlo';
 | 
			
		||||
      socket.write('EHLO testclient\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'ehlo' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'mail';
 | 
			
		||||
      socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'mail' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'rcpt';
 | 
			
		||||
      socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'rcpt' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'data';
 | 
			
		||||
      socket.write('DATA\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'data' && dataBuffer.includes('354')) {
 | 
			
		||||
      const email = [
 | 
			
		||||
        `From: sender@example.com`,
 | 
			
		||||
        `To: recipient@example.com`,
 | 
			
		||||
        `Subject: =?UTF-8?Q?Quoted=2DPrintable=20Test=20=F0=9F=8C=9F?=`,
 | 
			
		||||
        `Date: ${new Date().toUTCString()}`,
 | 
			
		||||
        `Message-ID: <qp-test-${Date.now()}@example.com>`,
 | 
			
		||||
        `MIME-Version: 1.0`,
 | 
			
		||||
        `Content-Type: text/plain; charset=utf-8`,
 | 
			
		||||
        `Content-Transfer-Encoding: quoted-printable`,
 | 
			
		||||
        '',
 | 
			
		||||
        'This is a test of quoted-printable encoding.',
 | 
			
		||||
        'Special characters: =C3=A9 =C3=A8 =C3=AA =C3=AB',
 | 
			
		||||
        'Long line that needs to be wrapped with soft line breaks at 76 character=',
 | 
			
		||||
        's per line to comply with MIME standards for quoted-printable encoding.',
 | 
			
		||||
        'Emoji: =F0=9F=98=80 =F0=9F=91=8D =F0=9F=8C=9F',
 | 
			
		||||
        '.',
 | 
			
		||||
        ''
 | 
			
		||||
      ].join('\r\n');
 | 
			
		||||
      
 | 
			
		||||
      socket.write(email);
 | 
			
		||||
      step = 'sent';
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
 | 
			
		||||
      if (!completed) {
 | 
			
		||||
        completed = true;
 | 
			
		||||
        console.log('Quoted-printable encoded email accepted');
 | 
			
		||||
        expect(true).toBeTrue();
 | 
			
		||||
        
 | 
			
		||||
        socket.write('QUIT\r\n');
 | 
			
		||||
        socket.end();
 | 
			
		||||
        done.resolve();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('error', (err) => {
 | 
			
		||||
    console.error('Socket error:', err);
 | 
			
		||||
    done.reject(err);
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  await done.promise;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('MIME Handling - Base64 encoding', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: 30000
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let dataBuffer = '';
 | 
			
		||||
  let step = 'greeting';
 | 
			
		||||
  let completed = false;
 | 
			
		||||
  
 | 
			
		||||
  socket.on('data', (data) => {
 | 
			
		||||
    dataBuffer += data.toString();
 | 
			
		||||
    console.log('Server response:', data.toString());
 | 
			
		||||
    
 | 
			
		||||
    if (step === 'greeting' && dataBuffer.includes('220 ')) {
 | 
			
		||||
      step = 'ehlo';
 | 
			
		||||
      socket.write('EHLO testclient\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'ehlo' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'mail';
 | 
			
		||||
      socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'mail' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'rcpt';
 | 
			
		||||
      socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'rcpt' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'data';
 | 
			
		||||
      socket.write('DATA\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'data' && dataBuffer.includes('354')) {
 | 
			
		||||
      const boundary = 'base64-test-boundary';
 | 
			
		||||
      const textContent = 'This is a test of base64 encoding with various content types.\nSpecial chars: éèêë\nEmoji: 😀 👍 🌟';
 | 
			
		||||
      const base64Content = Buffer.from(textContent).toString('base64');
 | 
			
		||||
      
 | 
			
		||||
      const email = [
 | 
			
		||||
        `From: sender@example.com`,
 | 
			
		||||
        `To: recipient@example.com`,
 | 
			
		||||
        `Subject: Base64 Encoding Test`,
 | 
			
		||||
        `Date: ${new Date().toUTCString()}`,
 | 
			
		||||
        `Message-ID: <base64-test-${Date.now()}@example.com>`,
 | 
			
		||||
        `MIME-Version: 1.0`,
 | 
			
		||||
        `Content-Type: multipart/mixed; boundary="${boundary}"`,
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: text/plain; charset=utf-8`,
 | 
			
		||||
        `Content-Transfer-Encoding: base64`,
 | 
			
		||||
        '',
 | 
			
		||||
        base64Content,
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: application/octet-stream`,
 | 
			
		||||
        `Content-Disposition: attachment; filename="binary.dat"`,
 | 
			
		||||
        `Content-Transfer-Encoding: base64`,
 | 
			
		||||
        '',
 | 
			
		||||
        'VGhpcyBpcyBiaW5hcnkgZGF0YSBmb3IgdGVzdGluZw==',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}--`,
 | 
			
		||||
        '.',
 | 
			
		||||
        ''
 | 
			
		||||
      ].join('\r\n');
 | 
			
		||||
      
 | 
			
		||||
      socket.write(email);
 | 
			
		||||
      step = 'sent';
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
 | 
			
		||||
      if (!completed) {
 | 
			
		||||
        completed = true;
 | 
			
		||||
        console.log('Base64 encoded email accepted');
 | 
			
		||||
        expect(true).toBeTrue();
 | 
			
		||||
        
 | 
			
		||||
        socket.write('QUIT\r\n');
 | 
			
		||||
        socket.end();
 | 
			
		||||
        done.resolve();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('error', (err) => {
 | 
			
		||||
    console.error('Socket error:', err);
 | 
			
		||||
    done.reject(err);
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  await done.promise;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('MIME Handling - Content-Disposition headers', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: 30000
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let dataBuffer = '';
 | 
			
		||||
  let step = 'greeting';
 | 
			
		||||
  let completed = false;
 | 
			
		||||
  
 | 
			
		||||
  socket.on('data', (data) => {
 | 
			
		||||
    dataBuffer += data.toString();
 | 
			
		||||
    console.log('Server response:', data.toString());
 | 
			
		||||
    
 | 
			
		||||
    if (step === 'greeting' && dataBuffer.includes('220 ')) {
 | 
			
		||||
      step = 'ehlo';
 | 
			
		||||
      socket.write('EHLO testclient\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'ehlo' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'mail';
 | 
			
		||||
      socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'mail' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'rcpt';
 | 
			
		||||
      socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'rcpt' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'data';
 | 
			
		||||
      socket.write('DATA\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'data' && dataBuffer.includes('354')) {
 | 
			
		||||
      const boundary = 'disposition-test-boundary';
 | 
			
		||||
      
 | 
			
		||||
      const email = [
 | 
			
		||||
        `From: sender@example.com`,
 | 
			
		||||
        `To: recipient@example.com`,
 | 
			
		||||
        `Subject: Content-Disposition Test`,
 | 
			
		||||
        `Date: ${new Date().toUTCString()}`,
 | 
			
		||||
        `Message-ID: <disposition-test-${Date.now()}@example.com>`,
 | 
			
		||||
        `MIME-Version: 1.0`,
 | 
			
		||||
        `Content-Type: multipart/mixed; boundary="${boundary}"`,
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: text/plain`,
 | 
			
		||||
        `Content-Disposition: inline`,
 | 
			
		||||
        '',
 | 
			
		||||
        'This is inline text content.',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: image/jpeg`,
 | 
			
		||||
        `Content-Disposition: attachment; filename="photo.jpg"`,
 | 
			
		||||
        `Content-Transfer-Encoding: base64`,
 | 
			
		||||
        '',
 | 
			
		||||
        '/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAEBAQ==',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: application/pdf`,
 | 
			
		||||
        `Content-Disposition: attachment; filename="report.pdf"; size=1234`,
 | 
			
		||||
        `Content-Description: Monthly Report`,
 | 
			
		||||
        '',
 | 
			
		||||
        'PDF content here',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: text/html`,
 | 
			
		||||
        `Content-Disposition: inline; filename="content.html"`,
 | 
			
		||||
        '',
 | 
			
		||||
        '<html><body>Inline HTML content</body></html>',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}--`,
 | 
			
		||||
        '.',
 | 
			
		||||
        ''
 | 
			
		||||
      ].join('\r\n');
 | 
			
		||||
      
 | 
			
		||||
      socket.write(email);
 | 
			
		||||
      step = 'sent';
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
 | 
			
		||||
      if (!completed) {
 | 
			
		||||
        completed = true;
 | 
			
		||||
        console.log('Email with various Content-Disposition headers accepted');
 | 
			
		||||
        expect(true).toBeTrue();
 | 
			
		||||
        
 | 
			
		||||
        socket.write('QUIT\r\n');
 | 
			
		||||
        socket.end();
 | 
			
		||||
        done.resolve();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('error', (err) => {
 | 
			
		||||
    console.error('Socket error:', err);
 | 
			
		||||
    done.reject(err);
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  await done.promise;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('MIME Handling - International character sets', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: 30000
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let dataBuffer = '';
 | 
			
		||||
  let step = 'greeting';
 | 
			
		||||
  let completed = false;
 | 
			
		||||
  
 | 
			
		||||
  socket.on('data', (data) => {
 | 
			
		||||
    dataBuffer += data.toString();
 | 
			
		||||
    console.log('Server response:', data.toString());
 | 
			
		||||
    
 | 
			
		||||
    if (step === 'greeting' && dataBuffer.includes('220 ')) {
 | 
			
		||||
      step = 'ehlo';
 | 
			
		||||
      socket.write('EHLO testclient\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'ehlo' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'mail';
 | 
			
		||||
      socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'mail' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'rcpt';
 | 
			
		||||
      socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'rcpt' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'data';
 | 
			
		||||
      socket.write('DATA\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'data' && dataBuffer.includes('354')) {
 | 
			
		||||
      const boundary = 'intl-charset-boundary';
 | 
			
		||||
      
 | 
			
		||||
      const email = [
 | 
			
		||||
        `From: sender@example.com`,
 | 
			
		||||
        `To: recipient@example.com`,
 | 
			
		||||
        `Subject: International Character Sets`,
 | 
			
		||||
        `Date: ${new Date().toUTCString()}`,
 | 
			
		||||
        `Message-ID: <intl-charset-${Date.now()}@example.com>`,
 | 
			
		||||
        `MIME-Version: 1.0`,
 | 
			
		||||
        `Content-Type: multipart/mixed; boundary="${boundary}"`,
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: text/plain; charset=utf-8`,
 | 
			
		||||
        '',
 | 
			
		||||
        'UTF-8: Français, Español, Deutsch, 中文, 日本語, 한국어, العربية',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: text/plain; charset=iso-8859-1`,
 | 
			
		||||
        '',
 | 
			
		||||
        'ISO-8859-1: Français, Español, Português',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: text/plain; charset=windows-1252`,
 | 
			
		||||
        '',
 | 
			
		||||
        'Windows-1252: Special chars: €‚ƒ„…†‡',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: text/plain; charset=shift_jis`,
 | 
			
		||||
        '',
 | 
			
		||||
        'Shift-JIS: Japanese text',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}--`,
 | 
			
		||||
        '.',
 | 
			
		||||
        ''
 | 
			
		||||
      ].join('\r\n');
 | 
			
		||||
      
 | 
			
		||||
      socket.write(email);
 | 
			
		||||
      step = 'sent';
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
 | 
			
		||||
      if (!completed) {
 | 
			
		||||
        completed = true;
 | 
			
		||||
        console.log('Email with international character sets accepted');
 | 
			
		||||
        expect(true).toBeTrue();
 | 
			
		||||
        
 | 
			
		||||
        socket.write('QUIT\r\n');
 | 
			
		||||
        socket.end();
 | 
			
		||||
        done.resolve();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('error', (err) => {
 | 
			
		||||
    console.error('Socket error:', err);
 | 
			
		||||
    done.reject(err);
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  await done.promise;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('cleanup - stop test server', async () => {
 | 
			
		||||
  await stopTestServer();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.start();
 | 
			
		||||
							
								
								
									
										476
									
								
								test/suite/email-processing/test.multiple-recipients.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										476
									
								
								test/suite/email-processing/test.multiple-recipients.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,476 @@
 | 
			
		||||
import { tap, expect } from '@push.rocks/tapbundle';
 | 
			
		||||
import * as net from 'net';
 | 
			
		||||
import * as path from 'path';
 | 
			
		||||
import { startTestServer, stopTestServer } from '../server.loader.js';
 | 
			
		||||
 | 
			
		||||
// Test configuration
 | 
			
		||||
const TEST_PORT = 2525;
 | 
			
		||||
const TEST_TIMEOUT = 15000;
 | 
			
		||||
 | 
			
		||||
let testServer: any;
 | 
			
		||||
 | 
			
		||||
// Setup
 | 
			
		||||
tap.test('setup - start SMTP server', async () => {
 | 
			
		||||
  testServer = await startTestServer();
 | 
			
		||||
  
 | 
			
		||||
  expect(testServer).toBeTypeofObject();
 | 
			
		||||
  expect(testServer.port).toEqual(TEST_PORT);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Test: Basic multiple recipients
 | 
			
		||||
tap.test('Multiple Recipients - should accept multiple valid recipients', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: TEST_TIMEOUT
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let receivedData = '';
 | 
			
		||||
  let currentStep = 'connecting';
 | 
			
		||||
  let recipientCount = 0;
 | 
			
		||||
  const recipients = [
 | 
			
		||||
    'recipient1@example.com',
 | 
			
		||||
    'recipient2@example.com',
 | 
			
		||||
    'recipient3@example.com'
 | 
			
		||||
  ];
 | 
			
		||||
  let acceptedRecipients = 0;
 | 
			
		||||
  
 | 
			
		||||
  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:<sender@example.com>\r\n');
 | 
			
		||||
    } else if (currentStep === 'mail_from' && receivedData.includes('250')) {
 | 
			
		||||
      currentStep = 'rcpt_to';
 | 
			
		||||
      socket.write(`RCPT TO:<${recipients[recipientCount]}>\r\n`);
 | 
			
		||||
    } else if (currentStep === 'rcpt_to') {
 | 
			
		||||
      if (receivedData.includes('250 OK')) {
 | 
			
		||||
        acceptedRecipients++;
 | 
			
		||||
        recipientCount++;
 | 
			
		||||
        
 | 
			
		||||
        if (recipientCount < recipients.length) {
 | 
			
		||||
          receivedData = ''; // Clear buffer for next response
 | 
			
		||||
          socket.write(`RCPT TO:<${recipients[recipientCount]}>\r\n`);
 | 
			
		||||
        } else {
 | 
			
		||||
          currentStep = 'data';
 | 
			
		||||
          socket.write('DATA\r\n');
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } else if (currentStep === 'data' && receivedData.includes('354')) {
 | 
			
		||||
      currentStep = 'email_content';
 | 
			
		||||
      const emailContent = `Subject: Multiple Recipients Test\r\nFrom: sender@example.com\r\nTo: ${recipients.join(', ')}\r\n\r\nThis email was sent to ${acceptedRecipients} recipients.\r\n`;
 | 
			
		||||
      socket.write(emailContent);
 | 
			
		||||
      socket.write('\r\n.\r\n');
 | 
			
		||||
    } else if (currentStep === 'email_content' && receivedData.includes('250')) {
 | 
			
		||||
      socket.write('QUIT\r\n');
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        socket.destroy();
 | 
			
		||||
        expect(acceptedRecipients).toEqual(recipients.length);
 | 
			
		||||
        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;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Test: Mixed valid and invalid recipients
 | 
			
		||||
tap.test('Multiple Recipients - should handle mix of valid and invalid recipients', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: TEST_TIMEOUT
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let receivedData = '';
 | 
			
		||||
  let currentStep = 'connecting';
 | 
			
		||||
  let recipientIndex = 0;
 | 
			
		||||
  const recipients = [
 | 
			
		||||
    'valid@example.com',
 | 
			
		||||
    'invalid-email',  // Invalid format
 | 
			
		||||
    'another.valid@example.com',
 | 
			
		||||
    '@example.com',   // Invalid format
 | 
			
		||||
    'third.valid@example.com'
 | 
			
		||||
  ];
 | 
			
		||||
  const recipientResults: Array<{ email: string, accepted: boolean }> = [];
 | 
			
		||||
  
 | 
			
		||||
  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:<sender@example.com>\r\n');
 | 
			
		||||
    } else if (currentStep === 'mail_from' && receivedData.includes('250')) {
 | 
			
		||||
      currentStep = 'rcpt_to';
 | 
			
		||||
      socket.write(`RCPT TO:<${recipients[recipientIndex]}>\r\n`);
 | 
			
		||||
    } else if (currentStep === 'rcpt_to') {
 | 
			
		||||
      const lines = receivedData.split('\r\n');
 | 
			
		||||
      const lastLine = lines[lines.length - 2] || lines[lines.length - 1];
 | 
			
		||||
      
 | 
			
		||||
      if (lastLine.match(/^\d{3}/)) {
 | 
			
		||||
        const accepted = lastLine.startsWith('250');
 | 
			
		||||
        recipientResults.push({
 | 
			
		||||
          email: recipients[recipientIndex],
 | 
			
		||||
          accepted: accepted
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        recipientIndex++;
 | 
			
		||||
        
 | 
			
		||||
        if (recipientIndex < recipients.length) {
 | 
			
		||||
          receivedData = ''; // Clear buffer
 | 
			
		||||
          socket.write(`RCPT TO:<${recipients[recipientIndex]}>\r\n`);
 | 
			
		||||
        } else {
 | 
			
		||||
          const acceptedCount = recipientResults.filter(r => r.accepted).length;
 | 
			
		||||
          
 | 
			
		||||
          if (acceptedCount > 0) {
 | 
			
		||||
            currentStep = 'data';
 | 
			
		||||
            socket.write('DATA\r\n');
 | 
			
		||||
          } else {
 | 
			
		||||
            socket.write('QUIT\r\n');
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
              socket.destroy();
 | 
			
		||||
              expect(acceptedCount).toEqual(0);
 | 
			
		||||
              done.resolve();
 | 
			
		||||
            }, 100);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } else if (currentStep === 'data' && receivedData.includes('354')) {
 | 
			
		||||
      currentStep = 'email_content';
 | 
			
		||||
      const acceptedEmails = recipientResults.filter(r => r.accepted).map(r => r.email);
 | 
			
		||||
      const emailContent = `Subject: Mixed Recipients Test\r\nFrom: sender@example.com\r\nTo: ${acceptedEmails.join(', ')}\r\n\r\nDelivered to ${acceptedEmails.length} valid recipients.\r\n`;
 | 
			
		||||
      socket.write(emailContent);
 | 
			
		||||
      socket.write('\r\n.\r\n');
 | 
			
		||||
    } else if (currentStep === 'email_content' && receivedData.includes('250')) {
 | 
			
		||||
      socket.write('QUIT\r\n');
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        socket.destroy();
 | 
			
		||||
        const acceptedCount = recipientResults.filter(r => r.accepted).length;
 | 
			
		||||
        const rejectedCount = recipientResults.filter(r => !r.accepted).length;
 | 
			
		||||
        expect(acceptedCount).toEqual(3); // 3 valid recipients
 | 
			
		||||
        expect(rejectedCount).toEqual(2); // 2 invalid recipients
 | 
			
		||||
        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;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Test: Large number of recipients
 | 
			
		||||
tap.test('Multiple Recipients - should handle many recipients', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: TEST_TIMEOUT
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let receivedData = '';
 | 
			
		||||
  let currentStep = 'connecting';
 | 
			
		||||
  let recipientCount = 0;
 | 
			
		||||
  const totalRecipients = 10;
 | 
			
		||||
  const recipients: string[] = [];
 | 
			
		||||
  for (let i = 1; i <= totalRecipients; i++) {
 | 
			
		||||
    recipients.push(`recipient${i}@example.com`);
 | 
			
		||||
  }
 | 
			
		||||
  let acceptedCount = 0;
 | 
			
		||||
  
 | 
			
		||||
  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:<sender@example.com>\r\n');
 | 
			
		||||
    } else if (currentStep === 'mail_from' && receivedData.includes('250')) {
 | 
			
		||||
      currentStep = 'rcpt_to';
 | 
			
		||||
      socket.write(`RCPT TO:<${recipients[recipientCount]}>\r\n`);
 | 
			
		||||
    } else if (currentStep === 'rcpt_to') {
 | 
			
		||||
      if (receivedData.includes('250')) {
 | 
			
		||||
        acceptedCount++;
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      recipientCount++;
 | 
			
		||||
      
 | 
			
		||||
      if (recipientCount < recipients.length) {
 | 
			
		||||
        receivedData = ''; // Clear buffer
 | 
			
		||||
        socket.write(`RCPT TO:<${recipients[recipientCount]}>\r\n`);
 | 
			
		||||
      } else {
 | 
			
		||||
        currentStep = 'data';
 | 
			
		||||
        socket.write('DATA\r\n');
 | 
			
		||||
      }
 | 
			
		||||
    } else if (currentStep === 'data' && receivedData.includes('354')) {
 | 
			
		||||
      currentStep = 'email_content';
 | 
			
		||||
      const emailContent = `Subject: Large Recipients Test\r\nFrom: sender@example.com\r\n\r\nSent to ${acceptedCount} recipients.\r\n`;
 | 
			
		||||
      socket.write(emailContent);
 | 
			
		||||
      socket.write('\r\n.\r\n');
 | 
			
		||||
    } else if (currentStep === 'email_content' && receivedData.includes('250')) {
 | 
			
		||||
      socket.write('QUIT\r\n');
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        socket.destroy();
 | 
			
		||||
        expect(acceptedCount).toBeGreaterThan(0);
 | 
			
		||||
        expect(acceptedCount).toBeLessThan(totalRecipients + 1);
 | 
			
		||||
        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;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Test: Duplicate recipients
 | 
			
		||||
tap.test('Multiple Recipients - should handle duplicate recipients', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: TEST_TIMEOUT
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let receivedData = '';
 | 
			
		||||
  let currentStep = 'connecting';
 | 
			
		||||
  let recipientCount = 0;
 | 
			
		||||
  const recipients = [
 | 
			
		||||
    'duplicate@example.com',
 | 
			
		||||
    'unique@example.com',
 | 
			
		||||
    'duplicate@example.com',  // Duplicate
 | 
			
		||||
    'another@example.com',
 | 
			
		||||
    'duplicate@example.com'   // Another duplicate
 | 
			
		||||
  ];
 | 
			
		||||
  const results: boolean[] = [];
 | 
			
		||||
  
 | 
			
		||||
  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:<sender@example.com>\r\n');
 | 
			
		||||
    } else if (currentStep === 'mail_from' && receivedData.includes('250')) {
 | 
			
		||||
      currentStep = 'rcpt_to';
 | 
			
		||||
      socket.write(`RCPT TO:<${recipients[recipientCount]}>\r\n`);
 | 
			
		||||
    } else if (currentStep === 'rcpt_to') {
 | 
			
		||||
      if (receivedData.match(/[245]\d{2}/)) {
 | 
			
		||||
        results.push(receivedData.includes('250'));
 | 
			
		||||
        recipientCount++;
 | 
			
		||||
        
 | 
			
		||||
        if (recipientCount < recipients.length) {
 | 
			
		||||
          receivedData = ''; // Clear buffer
 | 
			
		||||
          socket.write(`RCPT TO:<${recipients[recipientCount]}>\r\n`);
 | 
			
		||||
        } else {
 | 
			
		||||
          currentStep = 'data';
 | 
			
		||||
          socket.write('DATA\r\n');
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } else if (currentStep === 'data' && receivedData.includes('354')) {
 | 
			
		||||
      currentStep = 'email_content';
 | 
			
		||||
      const emailContent = `Subject: Duplicate Recipients Test\r\nFrom: sender@example.com\r\n\r\nTesting duplicate recipient handling.\r\n`;
 | 
			
		||||
      socket.write(emailContent);
 | 
			
		||||
      socket.write('\r\n.\r\n');
 | 
			
		||||
    } else if (currentStep === 'email_content' && receivedData.includes('250')) {
 | 
			
		||||
      socket.write('QUIT\r\n');
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        socket.destroy();
 | 
			
		||||
        expect(results.length).toEqual(recipients.length);
 | 
			
		||||
        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;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Test: No recipients (should fail DATA)
 | 
			
		||||
tap.test('Multiple Recipients - DATA should fail with no recipients', 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:<sender@example.com>\r\n');
 | 
			
		||||
    } else if (currentStep === 'mail_from' && receivedData.includes('250')) {
 | 
			
		||||
      // Skip RCPT TO, go directly to DATA
 | 
			
		||||
      currentStep = 'data_no_recipients';
 | 
			
		||||
      socket.write('DATA\r\n');
 | 
			
		||||
    } else if (currentStep === 'data_no_recipients' && receivedData.includes('503')) {
 | 
			
		||||
      socket.write('QUIT\r\n');
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        socket.destroy();
 | 
			
		||||
        expect(receivedData).toInclude('503'); // Bad sequence
 | 
			
		||||
        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;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Test: Recipients with different domains
 | 
			
		||||
tap.test('Multiple Recipients - should handle recipients from different domains', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: TEST_TIMEOUT
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let receivedData = '';
 | 
			
		||||
  let currentStep = 'connecting';
 | 
			
		||||
  let recipientCount = 0;
 | 
			
		||||
  const recipients = [
 | 
			
		||||
    'user1@example.com',
 | 
			
		||||
    'user2@test.com',
 | 
			
		||||
    'user3@localhost',
 | 
			
		||||
    'user4@example.org',
 | 
			
		||||
    'user5@subdomain.example.com'
 | 
			
		||||
  ];
 | 
			
		||||
  let acceptedCount = 0;
 | 
			
		||||
  
 | 
			
		||||
  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:<sender@example.com>\r\n');
 | 
			
		||||
    } else if (currentStep === 'mail_from' && receivedData.includes('250')) {
 | 
			
		||||
      currentStep = 'rcpt_to';
 | 
			
		||||
      socket.write(`RCPT TO:<${recipients[recipientCount]}>\r\n`);
 | 
			
		||||
    } else if (currentStep === 'rcpt_to') {
 | 
			
		||||
      if (receivedData.includes('250')) {
 | 
			
		||||
        acceptedCount++;
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      recipientCount++;
 | 
			
		||||
      
 | 
			
		||||
      if (recipientCount < recipients.length) {
 | 
			
		||||
        receivedData = ''; // Clear buffer
 | 
			
		||||
        socket.write(`RCPT TO:<${recipients[recipientCount]}>\r\n`);
 | 
			
		||||
      } else {
 | 
			
		||||
        if (acceptedCount > 0) {
 | 
			
		||||
          currentStep = 'data';
 | 
			
		||||
          socket.write('DATA\r\n');
 | 
			
		||||
        } else {
 | 
			
		||||
          socket.write('QUIT\r\n');
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            socket.destroy();
 | 
			
		||||
            done.resolve();
 | 
			
		||||
          }, 100);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } else if (currentStep === 'data' && receivedData.includes('354')) {
 | 
			
		||||
      currentStep = 'email_content';
 | 
			
		||||
      const emailContent = `Subject: Multi-domain Test\r\nFrom: sender@example.com\r\n\r\nDelivered to ${acceptedCount} recipients across different domains.\r\n`;
 | 
			
		||||
      socket.write(emailContent);
 | 
			
		||||
      socket.write('\r\n.\r\n');
 | 
			
		||||
    } else if (currentStep === 'email_content' && receivedData.includes('250')) {
 | 
			
		||||
      socket.write('QUIT\r\n');
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        socket.destroy();
 | 
			
		||||
        expect(acceptedCount).toBeGreaterThan(0);
 | 
			
		||||
        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 () => {
 | 
			
		||||
  if (testServer) {
 | 
			
		||||
    await stopTestServer();
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Start the test
 | 
			
		||||
tap.start();
 | 
			
		||||
							
								
								
									
										459
									
								
								test/suite/email-processing/test.special-character-handling.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										459
									
								
								test/suite/email-processing/test.special-character-handling.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,459 @@
 | 
			
		||||
import { tap, expect } from '@push.rocks/tapbundle';
 | 
			
		||||
import * as net from 'net';
 | 
			
		||||
import { startTestServer, stopTestServer, TEST_PORT, sendEmailWithRawSocket } from '../server.loader.js';
 | 
			
		||||
 | 
			
		||||
let testServer: any;
 | 
			
		||||
 | 
			
		||||
tap.test('setup - start test server', async () => {
 | 
			
		||||
  testServer = await startTestServer();
 | 
			
		||||
  await new Promise(resolve => setTimeout(resolve, 1000));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('Special Character Handling - Comprehensive Unicode test', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: 30000
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let dataBuffer = '';
 | 
			
		||||
  let step = 'greeting';
 | 
			
		||||
  let completed = false;
 | 
			
		||||
  
 | 
			
		||||
  socket.on('data', (data) => {
 | 
			
		||||
    dataBuffer += data.toString();
 | 
			
		||||
    console.log('Server response:', data.toString());
 | 
			
		||||
    
 | 
			
		||||
    if (step === 'greeting' && dataBuffer.includes('220 ')) {
 | 
			
		||||
      step = 'ehlo';
 | 
			
		||||
      socket.write('EHLO testclient\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'ehlo' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'mail';
 | 
			
		||||
      socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'mail' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'rcpt';
 | 
			
		||||
      socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'rcpt' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'data';
 | 
			
		||||
      socket.write('DATA\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'data' && dataBuffer.includes('354')) {
 | 
			
		||||
      const email = [
 | 
			
		||||
        `From: sender@example.com`,
 | 
			
		||||
        `To: recipient@example.com`,
 | 
			
		||||
        `Subject: Special Character Test - Unicode & Symbols ñáéíóú`,
 | 
			
		||||
        `Date: ${new Date().toUTCString()}`,
 | 
			
		||||
        `Message-ID: <special-chars-${Date.now()}@example.com>`,
 | 
			
		||||
        `MIME-Version: 1.0`,
 | 
			
		||||
        `Content-Type: text/plain; charset=utf-8`,
 | 
			
		||||
        `Content-Transfer-Encoding: 8bit`,
 | 
			
		||||
        '',
 | 
			
		||||
        'This email tests special character handling:',
 | 
			
		||||
        '',
 | 
			
		||||
        '=== UNICODE CHARACTERS ===',
 | 
			
		||||
        'Accented letters: àáâãäåæçèéêëìíîïñòóôõöøùúûüý',
 | 
			
		||||
        'German umlauts: äöüÄÖÜß',
 | 
			
		||||
        'Scandinavian: åäöÅÄÖ',
 | 
			
		||||
        'French: àâéèêëïîôœùûüÿç',
 | 
			
		||||
        'Spanish: ñáéíóúü¿¡',
 | 
			
		||||
        'Polish: ąćęłńóśźż',
 | 
			
		||||
        'Russian: абвгдеёжзийклмнопрстуфхцчшщъыьэюя',
 | 
			
		||||
        'Greek: αβγδεζηθικλμνξοπρστυφχψω',
 | 
			
		||||
        'Arabic: العربية',
 | 
			
		||||
        'Hebrew: עברית',
 | 
			
		||||
        'Chinese: 中文测试',
 | 
			
		||||
        'Japanese: 日本語テスト',
 | 
			
		||||
        'Korean: 한국어 테스트',
 | 
			
		||||
        'Thai: ภาษาไทย',
 | 
			
		||||
        '',
 | 
			
		||||
        '=== MATHEMATICAL SYMBOLS ===',
 | 
			
		||||
        'Math: ∑∏∫∆∇∂∞±×÷≠≤≥≈∝∪∩⊂⊃∈∀∃',
 | 
			
		||||
        'Greek letters: αβγδεζηθικλμνξοπρστυφχψω',
 | 
			
		||||
        'Arrows: ←→↑↓↔↕⇐⇒⇑⇓⇔⇕',
 | 
			
		||||
        '',
 | 
			
		||||
        '=== CURRENCY & SYMBOLS ===',
 | 
			
		||||
        'Currency: $€£¥¢₹₽₩₪₫₨₦₡₵₴₸₼₲₱',
 | 
			
		||||
        'Symbols: ©®™§¶†‡•…‰‱°℃℉№',
 | 
			
		||||
        'Punctuation: «»""''‚„‹›–—―‖‗''""‚„…‰′″‴‵‶‷‸‹›※‼‽⁇⁈⁉⁏⁐⁑⁒⁓⁔⁕⁖⁗⁘⁙⁚⁛⁜⁝⁞',
 | 
			
		||||
        '',
 | 
			
		||||
        '=== EMOJI & SYMBOLS ===',
 | 
			
		||||
        'Common: ☀☁☂☃☄★☆☇☈☉☊☋☌☍☎☏☐☑☒☓☔☕☖☗☘☙☚☛☜☝☞☟☠☡☢☣☤☥☦☧☨☩☪☫☬☭☮☯☰☱☲☳☴☵☶☷',
 | 
			
		||||
        'Smileys: ☺☻☹☿♀♁♂♃♄♅♆♇',
 | 
			
		||||
        'Hearts: ♡♢♣♤♥♦♧♨♩♪♫♬♭♮♯',
 | 
			
		||||
        '',
 | 
			
		||||
        '=== SPECIAL FORMATTING ===',
 | 
			
		||||
        'Zero-width chars: ',
 | 
			
		||||
        'Combining: e̊åa̋o̧ç',
 | 
			
		||||
        'Ligatures: fffiflffifflſtst',
 | 
			
		||||
        'Fractions: ½⅓⅔¼¾⅛⅜⅝⅞',
 | 
			
		||||
        'Superscript: ⁰¹²³⁴⁵⁶⁷⁸⁹',
 | 
			
		||||
        'Subscript: ₀₁₂₃₄₅₆₇₈₉',
 | 
			
		||||
        '',
 | 
			
		||||
        'End of special character test.',
 | 
			
		||||
        '.',
 | 
			
		||||
        ''
 | 
			
		||||
      ].join('\r\n');
 | 
			
		||||
      
 | 
			
		||||
      console.log('Sending email with comprehensive Unicode characters');
 | 
			
		||||
      socket.write(email);
 | 
			
		||||
      step = 'sent';
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
 | 
			
		||||
      if (!completed) {
 | 
			
		||||
        completed = true;
 | 
			
		||||
        console.log('Email with special characters accepted successfully');
 | 
			
		||||
        expect(true).toBeTrue();
 | 
			
		||||
        
 | 
			
		||||
        socket.write('QUIT\r\n');
 | 
			
		||||
        socket.end();
 | 
			
		||||
        done.resolve();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('error', (err) => {
 | 
			
		||||
    console.error('Socket error:', err);
 | 
			
		||||
    done.reject(err);
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  await done.promise;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('Special Character Handling - Control characters', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: 30000
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let dataBuffer = '';
 | 
			
		||||
  let step = 'greeting';
 | 
			
		||||
  let completed = false;
 | 
			
		||||
  
 | 
			
		||||
  socket.on('data', (data) => {
 | 
			
		||||
    dataBuffer += data.toString();
 | 
			
		||||
    console.log('Server response:', data.toString());
 | 
			
		||||
    
 | 
			
		||||
    if (step === 'greeting' && dataBuffer.includes('220 ')) {
 | 
			
		||||
      step = 'ehlo';
 | 
			
		||||
      socket.write('EHLO testclient\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'ehlo' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'mail';
 | 
			
		||||
      socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'mail' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'rcpt';
 | 
			
		||||
      socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'rcpt' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'data';
 | 
			
		||||
      socket.write('DATA\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'data' && dataBuffer.includes('354')) {
 | 
			
		||||
      const email = [
 | 
			
		||||
        `From: sender@example.com`,
 | 
			
		||||
        `To: recipient@example.com`,
 | 
			
		||||
        `Subject: Control Character Test`,
 | 
			
		||||
        `Date: ${new Date().toUTCString()}`,
 | 
			
		||||
        `Message-ID: <control-chars-${Date.now()}@example.com>`,
 | 
			
		||||
        `MIME-Version: 1.0`,
 | 
			
		||||
        `Content-Type: text/plain; charset=utf-8`,
 | 
			
		||||
        '',
 | 
			
		||||
        '=== CONTROL CHARACTERS TEST ===',
 | 
			
		||||
        'Tab character:	(between words)',
 | 
			
		||||
        'Non-breaking space: word word',
 | 
			
		||||
        'Soft hyphen: supercalifragilisticexpialidocious',
 | 
			
		||||
        'Vertical tab: word\x0Bword',
 | 
			
		||||
        'Form feed: word\x0Cword',
 | 
			
		||||
        'Backspace: word\x08word',
 | 
			
		||||
        '',
 | 
			
		||||
        '=== LINE ENDING TESTS ===',
 | 
			
		||||
        'Unix LF: Line1\nLine2',
 | 
			
		||||
        'Windows CRLF: Line3\r\nLine4',
 | 
			
		||||
        'Mac CR: Line5\rLine6',
 | 
			
		||||
        '',
 | 
			
		||||
        '=== BOUNDARY CHARACTERS ===',
 | 
			
		||||
        'SMTP boundary test: . (dot at start)',
 | 
			
		||||
        'Double dots: .. (escaped in SMTP)',
 | 
			
		||||
        'CRLF.CRLF sequence test',
 | 
			
		||||
        '.',
 | 
			
		||||
        ''
 | 
			
		||||
      ].join('\r\n');
 | 
			
		||||
      
 | 
			
		||||
      socket.write(email);
 | 
			
		||||
      step = 'sent';
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
 | 
			
		||||
      if (!completed) {
 | 
			
		||||
        completed = true;
 | 
			
		||||
        console.log('Email with control characters accepted');
 | 
			
		||||
        expect(true).toBeTrue();
 | 
			
		||||
        
 | 
			
		||||
        socket.write('QUIT\r\n');
 | 
			
		||||
        socket.end();
 | 
			
		||||
        done.resolve();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('error', (err) => {
 | 
			
		||||
    console.error('Socket error:', err);
 | 
			
		||||
    done.reject(err);
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  await done.promise;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('Special Character Handling - Subject header encoding', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: 30000
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let dataBuffer = '';
 | 
			
		||||
  let step = 'greeting';
 | 
			
		||||
  let completed = false;
 | 
			
		||||
  
 | 
			
		||||
  socket.on('data', (data) => {
 | 
			
		||||
    dataBuffer += data.toString();
 | 
			
		||||
    console.log('Server response:', data.toString());
 | 
			
		||||
    
 | 
			
		||||
    if (step === 'greeting' && dataBuffer.includes('220 ')) {
 | 
			
		||||
      step = 'ehlo';
 | 
			
		||||
      socket.write('EHLO testclient\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'ehlo' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'mail';
 | 
			
		||||
      socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'mail' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'rcpt';
 | 
			
		||||
      socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'rcpt' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'data';
 | 
			
		||||
      socket.write('DATA\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'data' && dataBuffer.includes('354')) {
 | 
			
		||||
      const email = [
 | 
			
		||||
        `From: sender@example.com`,
 | 
			
		||||
        `To: recipient@example.com`,
 | 
			
		||||
        `Subject: =?UTF-8?B?8J+YgCBFbW9qaSBpbiBTdWJqZWN0IOKcqCDwn4yI?=`,
 | 
			
		||||
        `Subject: =?UTF-8?Q?Quoted=2DPrintable=20Subject=20=C3=A1=C3=A9=C3=AD=C3=B3=C3=BA?=`,
 | 
			
		||||
        `Date: ${new Date().toUTCString()}`,
 | 
			
		||||
        `Message-ID: <encoded-subject-${Date.now()}@example.com>`,
 | 
			
		||||
        '',
 | 
			
		||||
        'Testing encoded subject headers with special characters.',
 | 
			
		||||
        '.',
 | 
			
		||||
        ''
 | 
			
		||||
      ].join('\r\n');
 | 
			
		||||
      
 | 
			
		||||
      socket.write(email);
 | 
			
		||||
      step = 'sent';
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
 | 
			
		||||
      if (!completed) {
 | 
			
		||||
        completed = true;
 | 
			
		||||
        console.log('Email with encoded subject headers accepted');
 | 
			
		||||
        expect(true).toBeTrue();
 | 
			
		||||
        
 | 
			
		||||
        socket.write('QUIT\r\n');
 | 
			
		||||
        socket.end();
 | 
			
		||||
        done.resolve();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('error', (err) => {
 | 
			
		||||
    console.error('Socket error:', err);
 | 
			
		||||
    done.reject(err);
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  await done.promise;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('Special Character Handling - Address headers with special chars', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: 30000
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let dataBuffer = '';
 | 
			
		||||
  let step = 'greeting';
 | 
			
		||||
  let completed = false;
 | 
			
		||||
  
 | 
			
		||||
  socket.on('data', (data) => {
 | 
			
		||||
    dataBuffer += data.toString();
 | 
			
		||||
    console.log('Server response:', data.toString());
 | 
			
		||||
    
 | 
			
		||||
    if (step === 'greeting' && dataBuffer.includes('220 ')) {
 | 
			
		||||
      step = 'ehlo';
 | 
			
		||||
      socket.write('EHLO testclient\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'ehlo' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'mail';
 | 
			
		||||
      socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'mail' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'rcpt';
 | 
			
		||||
      socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'rcpt' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'data';
 | 
			
		||||
      socket.write('DATA\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'data' && dataBuffer.includes('354')) {
 | 
			
		||||
      const email = [
 | 
			
		||||
        `From: "José García" <jose@example.com>`,
 | 
			
		||||
        `To: "François Müller" <francois@example.com>, "北京用户" <beijing@example.com>`,
 | 
			
		||||
        `Cc: =?UTF-8?B?IkFubmEgw4XDpMO2Ig==?= <anna@example.com>`,
 | 
			
		||||
        `Reply-To: "Søren Ñoño" <soren@example.com>`,
 | 
			
		||||
        `Subject: Special names in address headers`,
 | 
			
		||||
        `Date: ${new Date().toUTCString()}`,
 | 
			
		||||
        `Message-ID: <special-addrs-${Date.now()}@example.com>`,
 | 
			
		||||
        '',
 | 
			
		||||
        'Testing special characters in email addresses and display names.',
 | 
			
		||||
        '.',
 | 
			
		||||
        ''
 | 
			
		||||
      ].join('\r\n');
 | 
			
		||||
      
 | 
			
		||||
      socket.write(email);
 | 
			
		||||
      step = 'sent';
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
 | 
			
		||||
      if (!completed) {
 | 
			
		||||
        completed = true;
 | 
			
		||||
        console.log('Email with special characters in addresses accepted');
 | 
			
		||||
        expect(true).toBeTrue();
 | 
			
		||||
        
 | 
			
		||||
        socket.write('QUIT\r\n');
 | 
			
		||||
        socket.end();
 | 
			
		||||
        done.resolve();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('error', (err) => {
 | 
			
		||||
    console.error('Socket error:', err);
 | 
			
		||||
    done.reject(err);
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  await done.promise;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('Special Character Handling - Mixed encodings', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
  
 | 
			
		||||
  const socket = net.createConnection({
 | 
			
		||||
    host: 'localhost',
 | 
			
		||||
    port: TEST_PORT,
 | 
			
		||||
    timeout: 30000
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  let dataBuffer = '';
 | 
			
		||||
  let step = 'greeting';
 | 
			
		||||
  let completed = false;
 | 
			
		||||
  
 | 
			
		||||
  socket.on('data', (data) => {
 | 
			
		||||
    dataBuffer += data.toString();
 | 
			
		||||
    console.log('Server response:', data.toString());
 | 
			
		||||
    
 | 
			
		||||
    if (step === 'greeting' && dataBuffer.includes('220 ')) {
 | 
			
		||||
      step = 'ehlo';
 | 
			
		||||
      socket.write('EHLO testclient\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'ehlo' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'mail';
 | 
			
		||||
      socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'mail' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'rcpt';
 | 
			
		||||
      socket.write('RCPT TO:<recipient@example.com>\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'rcpt' && dataBuffer.includes('250')) {
 | 
			
		||||
      step = 'data';
 | 
			
		||||
      socket.write('DATA\r\n');
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'data' && dataBuffer.includes('354')) {
 | 
			
		||||
      const boundary = 'mixed-encoding-boundary';
 | 
			
		||||
      
 | 
			
		||||
      const email = [
 | 
			
		||||
        `From: sender@example.com`,
 | 
			
		||||
        `To: recipient@example.com`,
 | 
			
		||||
        `Subject: Mixed Encoding Test`,
 | 
			
		||||
        `Date: ${new Date().toUTCString()}`,
 | 
			
		||||
        `Message-ID: <mixed-enc-${Date.now()}@example.com>`,
 | 
			
		||||
        `MIME-Version: 1.0`,
 | 
			
		||||
        `Content-Type: multipart/mixed; boundary="${boundary}"`,
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: text/plain; charset=utf-8`,
 | 
			
		||||
        `Content-Transfer-Encoding: 8bit`,
 | 
			
		||||
        '',
 | 
			
		||||
        'UTF-8 part: ñáéíóú 中文 日本語',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: text/plain; charset=iso-8859-1`,
 | 
			
		||||
        `Content-Transfer-Encoding: quoted-printable`,
 | 
			
		||||
        '',
 | 
			
		||||
        'ISO-8859-1 part: =F1=E1=E9=ED=F3=FA',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: text/plain; charset=windows-1252`,
 | 
			
		||||
        '',
 | 
			
		||||
        'Windows-1252 part: €‚ƒ„…†‡',
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}`,
 | 
			
		||||
        `Content-Type: text/plain; charset=utf-16`,
 | 
			
		||||
        `Content-Transfer-Encoding: base64`,
 | 
			
		||||
        '',
 | 
			
		||||
        Buffer.from('UTF-16 text: ñoño', 'utf16le').toString('base64'),
 | 
			
		||||
        '',
 | 
			
		||||
        `--${boundary}--`,
 | 
			
		||||
        '.',
 | 
			
		||||
        ''
 | 
			
		||||
      ].join('\r\n');
 | 
			
		||||
      
 | 
			
		||||
      socket.write(email);
 | 
			
		||||
      step = 'sent';
 | 
			
		||||
      dataBuffer = '';
 | 
			
		||||
    } else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
 | 
			
		||||
      if (!completed) {
 | 
			
		||||
        completed = true;
 | 
			
		||||
        console.log('Email with mixed character encodings accepted');
 | 
			
		||||
        expect(true).toBeTrue();
 | 
			
		||||
        
 | 
			
		||||
        socket.write('QUIT\r\n');
 | 
			
		||||
        socket.end();
 | 
			
		||||
        done.resolve();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  socket.on('error', (err) => {
 | 
			
		||||
    console.error('Socket error:', err);
 | 
			
		||||
    done.reject(err);
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  await done.promise;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('cleanup - stop test server', async () => {
 | 
			
		||||
  await stopTestServer();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.start();
 | 
			
		||||
		Reference in New Issue
	
	Block a user