import { tap, expect } from '@git.zone/tstest/tapbundle'; import * as net from 'net'; import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; import type { SmtpServer } from '../../../ts/mail/delivery/smtpserver/index.js'; const TEST_PORT = 2525; let testServer: SmtpServer; 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:\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); // 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: `, `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"`, '', '', '', ' value', ' ñoño ∑∆≈', '', '', `--${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"`, '', '', 'Test', '

HTML Attachment

Content with markup

', '', '', `--${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).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'; // 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: `, `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).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`, '', 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==', '', `--${boundary}`, `Content-Type: image/png`, `Content-ID: `, `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).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: 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).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(); }); tap.start();