import { tap, expect } from '@git.zone/tstest/tapbundle'; import * as net from 'net'; import * as fs from 'fs'; import * as path from 'path'; import { startTestServer, stopTestServer } from '../../helpers/server.loader.js' import type { ITestServer } from '../../helpers/server.loader.js'; const TEST_PORT = 2525; const SAMPLE_FILES_DIR = path.join(process.cwd(), '.nogit', 'sample-files'); let testServer: ITestServer; // Helper function to read and encode files function readFileAsBase64(filePath: string): string { try { const fileContent = fs.readFileSync(filePath); return fileContent.toString('base64'); } catch (err) { console.error(`Failed to read file ${filePath}:`, err); return ''; } } tap.test('setup - start test server', async () => { testServer = await startTestServer({ port: TEST_PORT }); 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:\r\n'); dataBuffer = ''; } else if (step === 'mail' && dataBuffer.includes('250')) { step = 'rcpt'; socket.write('RCPT TO:\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); // Read real files from sample directory const sampleImage = readFileAsBase64(path.join(SAMPLE_FILES_DIR, '003-pdflatex-image/image.jpg')); const minimalPdf = readFileAsBase64(path.join(SAMPLE_FILES_DIR, '001-trivial/minimal-document.pdf')); const multiPagePdf = readFileAsBase64(path.join(SAMPLE_FILES_DIR, '004-pdflatex-4-pages/pdflatex-4-pages.pdf')); const pdfWithAttachment = readFileAsBase64(path.join(SAMPLE_FILES_DIR, '025-attachment/with-attachment.pdf')); const email = [ `From: sender@example.com`, `To: recipient@example.com`, `Subject: Attachment Handling Test - Multiple Types`, `Date: ${new Date().toUTCString()}`, `Message-ID: `, `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/jpeg`, `Content-Disposition: attachment; filename="sample-image.jpg"`, `Content-Transfer-Encoding: base64`, '', sampleImage, '', `--${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"`, '', '', '', ' value', ' ñoño ∑∆≈', '', '', `--${boundary}`, `Content-Type: application/pdf`, `Content-Disposition: attachment; filename="minimal-document.pdf"`, `Content-Transfer-Encoding: base64`, '', minimalPdf, '', `--${boundary}`, `Content-Type: application/pdf`, `Content-Disposition: attachment; filename="multi-page-document.pdf"`, `Content-Transfer-Encoding: base64`, '', multiPagePdf, '', `--${boundary}`, `Content-Type: application/pdf`, `Content-Disposition: attachment; filename="pdf-with-embedded-attachment.pdf"`, `Content-Transfer-Encoding: base64`, '', pdfWithAttachment, '', `--${boundary}`, `Content-Type: text/html; charset=utf-8`, `Content-Disposition: attachment; filename="webpage.html"`, '', '', 'Test', '

HTML Attachment

Content with markup

', '', '', `--${boundary}--`, '.', '' ].join('\r\n'); console.log('Sending email with 10 different attachment types including real PDFs'); 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).toEqual(true); 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:\r\n'); dataBuffer = ''; } else if (step === 'mail' && dataBuffer.includes('250')) { step = 'rcpt'; socket.write('RCPT TO:\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'; // Use a real large PDF file const largePdf = readFileAsBase64(path.join(SAMPLE_FILES_DIR, '009-pdflatex-geotopo/GeoTopo.pdf')); const largePdfSize = Buffer.from(largePdf, 'base64').length; console.log(`Large PDF size: ${(largePdfSize / 1024).toFixed(2)}KB`); const email = [ `From: sender@example.com`, `To: recipient@example.com`, `Subject: Large Attachment Test`, `Date: ${new Date().toUTCString()}`, `Message-ID: `, `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/pdf`, `Content-Disposition: attachment; filename="large-geotopo.pdf"`, `Content-Transfer-Encoding: base64`, '', largePdf, '', `--${boundary}--`, '.', '' ].join('\r\n'); console.log(`Sending email with large PDF attachment (${(largePdfSize / 1024).toFixed(2)}KB)`); 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).toEqual(true); 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:\r\n'); dataBuffer = ''; } else if (step === 'mail' && dataBuffer.includes('250')) { step = 'rcpt'; socket.write('RCPT TO:\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: `, `MIME-Version: 1.0`, `Content-Type: multipart/related; boundary="${boundary}"`, '', `--${boundary}`, `Content-Type: text/html`, '', '', '

This email has inline images:

', '', '', '', '', `--${boundary}`, `Content-Type: image/png`, `Content-ID: `, `Content-Disposition: inline; filename="inline1.png"`, `Content-Transfer-Encoding: base64`, '', readFileAsBase64(path.join(SAMPLE_FILES_DIR, '008-reportlab-inline-image/smile.png')) || 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==', '', `--${boundary}`, `Content-Type: image/png`, `Content-ID: `, `Content-Disposition: inline; filename="inline2.png"`, `Content-Transfer-Encoding: base64`, '', readFileAsBase64(path.join(SAMPLE_FILES_DIR, '019-grayscale-image/page-0-X0.png')) || 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==', '', `--${boundary}`, `Content-Type: application/pdf`, `Content-Disposition: attachment; filename="document.pdf"`, `Content-Transfer-Encoding: base64`, '', readFileAsBase64(path.join(SAMPLE_FILES_DIR, '013-reportlab-overlay/reportlab-overlay.pdf')) || '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).toEqual(true); 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:\r\n'); dataBuffer = ''; } else if (step === 'mail' && dataBuffer.includes('250')) { step = 'rcpt'; socket.write('RCPT TO:\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: `, `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).toEqual(true); 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:\r\n'); dataBuffer = ''; } else if (step === 'mail' && dataBuffer.includes('250')) { step = 'rcpt'; socket.write('RCPT TO:\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: `, `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: application/pdf`, `Content-Disposition: attachment; filename="broken.pdf"`, `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).toEqual(true); 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(testServer); }); export default tap.start();