2025-05-23 19:09:30 +00:00
|
|
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
2025-05-23 19:03:44 +00:00
|
|
|
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();
|