535 lines
18 KiB
TypeScript
535 lines
18 KiB
TypeScript
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
import * as plugins from './plugins.js';
|
|
import { createTestServer } from '../../helpers/server.loader.js';
|
|
import { createSmtpClient } from '../../helpers/smtp.client.js';
|
|
|
|
tap.test('CEDGE-05: should handle encoding issues gracefully', async (tools) => {
|
|
const testId = 'CEDGE-05-encoding-issues';
|
|
console.log(`\n${testId}: Testing encoding issue handling...`);
|
|
|
|
let scenarioCount = 0;
|
|
|
|
// Scenario 1: Mixed character encodings in email content
|
|
await (async () => {
|
|
scenarioCount++;
|
|
console.log(`\nScenario ${scenarioCount}: Testing mixed character encodings`);
|
|
|
|
const testServer = await createTestServer({
|
|
onConnection: async (socket) => {
|
|
console.log(' [Server] Client connected');
|
|
socket.write('220 mail.example.com ESMTP\r\n');
|
|
|
|
let inData = false;
|
|
let messageData = '';
|
|
|
|
socket.on('data', (data) => {
|
|
const text = data.toString();
|
|
|
|
if (inData) {
|
|
messageData += text;
|
|
if (text.includes('\r\n.\r\n')) {
|
|
inData = false;
|
|
console.log(` [Server] Received message data (${messageData.length} bytes)`);
|
|
|
|
// Check for various encodings
|
|
const hasUtf8 = /[\u0080-\uFFFF]/.test(messageData);
|
|
const hasBase64 = /Content-Transfer-Encoding:\s*base64/i.test(messageData);
|
|
const hasQuotedPrintable = /Content-Transfer-Encoding:\s*quoted-printable/i.test(messageData);
|
|
|
|
console.log(` [Server] Encodings detected: UTF-8=${hasUtf8}, Base64=${hasBase64}, QP=${hasQuotedPrintable}`);
|
|
|
|
socket.write('250 OK\r\n');
|
|
messageData = '';
|
|
}
|
|
return;
|
|
}
|
|
|
|
const command = text.trim();
|
|
console.log(` [Server] Received: ${command}`);
|
|
|
|
if (command.startsWith('EHLO')) {
|
|
socket.write('250-mail.example.com\r\n');
|
|
socket.write('250-8BITMIME\r\n');
|
|
socket.write('250-SMTPUTF8\r\n');
|
|
socket.write('250 OK\r\n');
|
|
} else if (command.startsWith('MAIL FROM:')) {
|
|
socket.write('250 OK\r\n');
|
|
} else if (command.startsWith('RCPT TO:')) {
|
|
socket.write('250 OK\r\n');
|
|
} else if (command === 'DATA') {
|
|
socket.write('354 Start mail input\r\n');
|
|
inData = true;
|
|
} else if (command === 'QUIT') {
|
|
socket.write('221 Bye\r\n');
|
|
socket.end();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
const smtpClient = createSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false
|
|
});
|
|
|
|
// Email with mixed encodings
|
|
const email = new plugins.smartmail.Email({
|
|
from: 'sender@example.com',
|
|
to: ['recipient@example.com'],
|
|
subject: 'Test with émojis 🎉 and spéçiål characters',
|
|
text: 'Plain text with Unicode: café, naïve, 你好, مرحبا',
|
|
html: '<p>HTML with entities: café, naïve, and emoji 🌟</p>',
|
|
attachments: [{
|
|
filename: 'tëst-filé.txt',
|
|
content: 'Attachment content with special chars: ñ, ü, ß'
|
|
}]
|
|
});
|
|
|
|
const result = await smtpClient.sendMail(email);
|
|
console.log(` Result: ${result.messageId ? 'Success' : 'Failed'}`);
|
|
expect(result).toBeDefined();
|
|
expect(result.messageId).toBeDefined();
|
|
|
|
await testServer.server.close();
|
|
})();
|
|
|
|
// Scenario 2: Invalid UTF-8 sequences
|
|
await (async () => {
|
|
scenarioCount++;
|
|
console.log(`\nScenario ${scenarioCount}: Testing invalid UTF-8 sequences`);
|
|
|
|
const testServer = await createTestServer({
|
|
onConnection: async (socket) => {
|
|
console.log(' [Server] Client connected');
|
|
socket.write('220 mail.example.com ESMTP\r\n');
|
|
|
|
let inData = false;
|
|
|
|
socket.on('data', (data) => {
|
|
if (inData) {
|
|
if (data.toString().includes('\r\n.\r\n')) {
|
|
inData = false;
|
|
socket.write('250 OK\r\n');
|
|
}
|
|
return;
|
|
}
|
|
|
|
const command = data.toString().trim();
|
|
console.log(` [Server] Received: ${command}`);
|
|
|
|
if (command.startsWith('EHLO')) {
|
|
socket.write('250-mail.example.com\r\n');
|
|
socket.write('250-8BITMIME\r\n');
|
|
socket.write('250 OK\r\n');
|
|
} else if (command.startsWith('MAIL FROM:')) {
|
|
// Check for invalid UTF-8 in email address
|
|
const hasInvalidUtf8 = Buffer.from(command).some((byte, i, arr) => {
|
|
if (byte >= 0x80) {
|
|
// Check if it's valid UTF-8
|
|
if ((byte & 0xE0) === 0xC0) {
|
|
return i + 1 >= arr.length || (arr[i + 1] & 0xC0) !== 0x80;
|
|
}
|
|
// Add more UTF-8 validation as needed
|
|
}
|
|
return false;
|
|
});
|
|
|
|
if (hasInvalidUtf8) {
|
|
socket.write('501 5.5.4 Invalid UTF-8 in address\r\n');
|
|
} else {
|
|
socket.write('250 OK\r\n');
|
|
}
|
|
} else if (command.startsWith('RCPT TO:')) {
|
|
socket.write('250 OK\r\n');
|
|
} else if (command === 'DATA') {
|
|
socket.write('354 Start mail input\r\n');
|
|
inData = true;
|
|
} else if (command === 'QUIT') {
|
|
socket.write('221 Bye\r\n');
|
|
socket.end();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
const smtpClient = createSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false
|
|
});
|
|
|
|
// Create email with potentially problematic content
|
|
const email = new plugins.smartmail.Email({
|
|
from: 'sender@example.com',
|
|
to: ['recipient@example.com'],
|
|
subject: 'Test with various encodings',
|
|
text: 'Testing text with special chars',
|
|
headers: {
|
|
'X-Custom-Header': 'Test value with special chars'
|
|
}
|
|
});
|
|
|
|
const result = await smtpClient.sendMail(email);
|
|
console.log(` Result: ${result.messageId ? 'Success' : 'Failed'}`);
|
|
expect(result).toBeDefined();
|
|
|
|
await testServer.server.close();
|
|
})();
|
|
|
|
// Scenario 3: Base64 encoding edge cases
|
|
await (async () => {
|
|
scenarioCount++;
|
|
console.log(`\nScenario ${scenarioCount}: Testing Base64 encoding edge cases`);
|
|
|
|
const testServer = await createTestServer({
|
|
onConnection: async (socket) => {
|
|
console.log(' [Server] Client connected');
|
|
socket.write('220 mail.example.com ESMTP\r\n');
|
|
|
|
let inData = false;
|
|
let messageData = '';
|
|
|
|
socket.on('data', (data) => {
|
|
if (inData) {
|
|
messageData += data.toString();
|
|
if (messageData.includes('\r\n.\r\n')) {
|
|
inData = false;
|
|
|
|
// Check for base64 content
|
|
const base64Match = messageData.match(/Content-Transfer-Encoding:\s*base64\r?\n\r?\n([^\r\n]+)/i);
|
|
if (base64Match) {
|
|
const base64Content = base64Match[1];
|
|
console.log(` [Server] Found base64 content: ${base64Content.substring(0, 50)}...`);
|
|
|
|
// Verify it's valid base64
|
|
const isValidBase64 = /^[A-Za-z0-9+/]*={0,2}$/.test(base64Content.replace(/\s/g, ''));
|
|
console.log(` [Server] Base64 valid: ${isValidBase64}`);
|
|
}
|
|
|
|
socket.write('250 OK\r\n');
|
|
messageData = '';
|
|
}
|
|
return;
|
|
}
|
|
|
|
const command = data.toString().trim();
|
|
console.log(` [Server] Received: ${command}`);
|
|
|
|
if (command.startsWith('EHLO')) {
|
|
socket.write('250-mail.example.com\r\n');
|
|
socket.write('250 OK\r\n');
|
|
} else if (command.startsWith('MAIL FROM:')) {
|
|
socket.write('250 OK\r\n');
|
|
} else if (command.startsWith('RCPT TO:')) {
|
|
socket.write('250 OK\r\n');
|
|
} else if (command === 'DATA') {
|
|
socket.write('354 Start mail input\r\n');
|
|
inData = true;
|
|
} else if (command === 'QUIT') {
|
|
socket.write('221 Bye\r\n');
|
|
socket.end();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
const smtpClient = createSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false
|
|
});
|
|
|
|
// Create various sizes of binary content
|
|
const sizes = [0, 1, 2, 3, 57, 58, 59, 76, 77]; // Edge cases for base64 line wrapping
|
|
|
|
for (const size of sizes) {
|
|
const binaryContent = Buffer.alloc(size);
|
|
for (let i = 0; i < size; i++) {
|
|
binaryContent[i] = i % 256;
|
|
}
|
|
|
|
const email = new plugins.smartmail.Email({
|
|
from: 'sender@example.com',
|
|
to: ['recipient@example.com'],
|
|
subject: `Base64 test with ${size} bytes`,
|
|
text: 'Testing base64 encoding',
|
|
attachments: [{
|
|
filename: `test-${size}.bin`,
|
|
content: binaryContent
|
|
}]
|
|
});
|
|
|
|
console.log(` Testing with ${size} byte attachment...`);
|
|
const result = await smtpClient.sendMail(email);
|
|
expect(result).toBeDefined();
|
|
expect(result.messageId).toBeDefined();
|
|
}
|
|
|
|
await testServer.server.close();
|
|
})();
|
|
|
|
// Scenario 4: Quoted-printable encoding edge cases
|
|
await (async () => {
|
|
scenarioCount++;
|
|
console.log(`\nScenario ${scenarioCount}: Testing quoted-printable encoding`);
|
|
|
|
const testServer = await createTestServer({
|
|
onConnection: async (socket) => {
|
|
console.log(' [Server] Client connected');
|
|
socket.write('220 mail.example.com ESMTP\r\n');
|
|
|
|
let inData = false;
|
|
let messageData = '';
|
|
|
|
socket.on('data', (data) => {
|
|
if (inData) {
|
|
messageData += data.toString();
|
|
if (messageData.includes('\r\n.\r\n')) {
|
|
inData = false;
|
|
|
|
// Check for quoted-printable content
|
|
if (/Content-Transfer-Encoding:\s*quoted-printable/i.test(messageData)) {
|
|
console.log(' [Server] Found quoted-printable content');
|
|
|
|
// Check for proper QP encoding
|
|
const qpLines = messageData.split('\r\n');
|
|
const longLines = qpLines.filter(line => line.length > 76);
|
|
if (longLines.length > 0) {
|
|
console.log(` [Server] Warning: ${longLines.length} lines exceed 76 characters`);
|
|
}
|
|
}
|
|
|
|
socket.write('250 OK\r\n');
|
|
messageData = '';
|
|
}
|
|
return;
|
|
}
|
|
|
|
const command = data.toString().trim();
|
|
console.log(` [Server] Received: ${command}`);
|
|
|
|
if (command.startsWith('EHLO')) {
|
|
socket.write('250-mail.example.com\r\n');
|
|
socket.write('250 OK\r\n');
|
|
} else if (command.startsWith('MAIL FROM:')) {
|
|
socket.write('250 OK\r\n');
|
|
} else if (command.startsWith('RCPT TO:')) {
|
|
socket.write('250 OK\r\n');
|
|
} else if (command === 'DATA') {
|
|
socket.write('354 Start mail input\r\n');
|
|
inData = true;
|
|
} else if (command === 'QUIT') {
|
|
socket.write('221 Bye\r\n');
|
|
socket.end();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
const smtpClient = createSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false
|
|
});
|
|
|
|
// Test with content that requires quoted-printable encoding
|
|
const email = new plugins.smartmail.Email({
|
|
from: 'sender@example.com',
|
|
to: ['recipient@example.com'],
|
|
subject: 'Quoted-printable test',
|
|
text: [
|
|
'Line with special chars: café, naïve',
|
|
'Very long line that exceeds the 76 character limit and should be properly wrapped when encoded with quoted-printable encoding',
|
|
'Line with = sign and trailing spaces ',
|
|
'Line ending with =',
|
|
'Tést with various spëcial characters: ñ, ü, ß, ø, å'
|
|
].join('\n')
|
|
});
|
|
|
|
const result = await smtpClient.sendMail(email);
|
|
console.log(` Result: ${result.messageId ? 'Success' : 'Failed'}`);
|
|
expect(result).toBeDefined();
|
|
expect(result.messageId).toBeDefined();
|
|
|
|
await testServer.server.close();
|
|
})();
|
|
|
|
// Scenario 5: Header encoding (RFC 2047)
|
|
await (async () => {
|
|
scenarioCount++;
|
|
console.log(`\nScenario ${scenarioCount}: Testing header encoding (RFC 2047)`);
|
|
|
|
const testServer = await createTestServer({
|
|
onConnection: async (socket) => {
|
|
console.log(' [Server] Client connected');
|
|
socket.write('220 mail.example.com ESMTP\r\n');
|
|
|
|
let inData = false;
|
|
let headers: string[] = [];
|
|
|
|
socket.on('data', (data) => {
|
|
const text = data.toString();
|
|
|
|
if (inData) {
|
|
if (!text.startsWith('\r\n') && text.includes(':')) {
|
|
headers.push(text.split('\r\n')[0]);
|
|
}
|
|
|
|
if (text.includes('\r\n.\r\n')) {
|
|
inData = false;
|
|
|
|
// Check encoded headers
|
|
const encodedHeaders = headers.filter(h => h.includes('=?'));
|
|
console.log(` [Server] Found ${encodedHeaders.length} encoded headers`);
|
|
encodedHeaders.forEach(h => {
|
|
const match = h.match(/=\?([^?]+)\?([BQ])\?([^?]+)\?=/);
|
|
if (match) {
|
|
console.log(` [Server] Encoded header: charset=${match[1]}, encoding=${match[2]}`);
|
|
}
|
|
});
|
|
|
|
socket.write('250 OK\r\n');
|
|
headers = [];
|
|
}
|
|
return;
|
|
}
|
|
|
|
const command = text.trim();
|
|
console.log(` [Server] Received: ${command}`);
|
|
|
|
if (command.startsWith('EHLO')) {
|
|
socket.write('250-mail.example.com\r\n');
|
|
socket.write('250-SMTPUTF8\r\n');
|
|
socket.write('250 OK\r\n');
|
|
} else if (command.startsWith('MAIL FROM:')) {
|
|
socket.write('250 OK\r\n');
|
|
} else if (command.startsWith('RCPT TO:')) {
|
|
socket.write('250 OK\r\n');
|
|
} else if (command === 'DATA') {
|
|
socket.write('354 Start mail input\r\n');
|
|
inData = true;
|
|
} else if (command === 'QUIT') {
|
|
socket.write('221 Bye\r\n');
|
|
socket.end();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
const smtpClient = createSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false
|
|
});
|
|
|
|
// Test various header encodings
|
|
const testCases = [
|
|
{
|
|
subject: 'Simple ASCII subject',
|
|
from: { name: 'John Doe', address: 'john@example.com' }
|
|
},
|
|
{
|
|
subject: 'Subject with émojis 🎉 and spéçiål çhåracters',
|
|
from: { name: 'Jöhn Døe', address: 'john@example.com' }
|
|
},
|
|
{
|
|
subject: 'Japanese: こんにちは, Chinese: 你好, Arabic: مرحبا',
|
|
from: { name: '山田太郎', address: 'yamada@example.com' }
|
|
},
|
|
{
|
|
subject: 'Very long subject that contains special characters and should be encoded and folded properly: café, naïve, résumé, piñata',
|
|
from: { name: 'Sender with a véry løng nåme that éxceeds normal limits', address: 'sender@example.com' }
|
|
}
|
|
];
|
|
|
|
for (const testCase of testCases) {
|
|
console.log(` Testing: "${testCase.subject.substring(0, 50)}..."`);
|
|
|
|
const email = new plugins.smartmail.Email({
|
|
from: testCase.from,
|
|
to: ['recipient@example.com'],
|
|
subject: testCase.subject,
|
|
text: 'Testing header encoding',
|
|
headers: {
|
|
'X-Custom': `Custom header with special chars: ${testCase.subject.substring(0, 20)}`
|
|
}
|
|
});
|
|
|
|
const result = await smtpClient.sendMail(email);
|
|
expect(result).toBeDefined();
|
|
expect(result.messageId).toBeDefined();
|
|
}
|
|
|
|
await testServer.server.close();
|
|
})();
|
|
|
|
// Scenario 6: Content-Type charset mismatches
|
|
await (async () => {
|
|
scenarioCount++;
|
|
console.log(`\nScenario ${scenarioCount}: Testing Content-Type charset handling`);
|
|
|
|
const testServer = await createTestServer({
|
|
onConnection: async (socket) => {
|
|
console.log(' [Server] Client connected');
|
|
socket.write('220 mail.example.com ESMTP\r\n');
|
|
|
|
let inData = false;
|
|
|
|
socket.on('data', (data) => {
|
|
if (inData) {
|
|
if (data.toString().includes('\r\n.\r\n')) {
|
|
inData = false;
|
|
socket.write('250 OK\r\n');
|
|
}
|
|
return;
|
|
}
|
|
|
|
const command = data.toString().trim();
|
|
console.log(` [Server] Received: ${command}`);
|
|
|
|
if (command.startsWith('EHLO')) {
|
|
socket.write('250-mail.example.com\r\n');
|
|
socket.write('250 OK\r\n');
|
|
} else if (command.startsWith('MAIL FROM:')) {
|
|
socket.write('250 OK\r\n');
|
|
} else if (command.startsWith('RCPT TO:')) {
|
|
socket.write('250 OK\r\n');
|
|
} else if (command === 'DATA') {
|
|
socket.write('354 Start mail input\r\n');
|
|
inData = true;
|
|
} else if (command === 'QUIT') {
|
|
socket.write('221 Bye\r\n');
|
|
socket.end();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
const smtpClient = createSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false
|
|
});
|
|
|
|
// Test with different charset declarations
|
|
const email = new plugins.smartmail.Email({
|
|
from: 'sender@example.com',
|
|
to: ['recipient@example.com'],
|
|
subject: 'Charset test',
|
|
text: 'Text with special chars: é, ñ, ü',
|
|
html: '<p>HTML with different chars: café, naïve</p>',
|
|
headers: {
|
|
'Content-Type': 'text/plain; charset=iso-8859-1' // Mismatch with actual UTF-8 content
|
|
}
|
|
});
|
|
|
|
const result = await smtpClient.sendMail(email);
|
|
console.log(` Result: ${result.messageId ? 'Success' : 'Failed'}`);
|
|
expect(result).toBeDefined();
|
|
|
|
await testServer.server.close();
|
|
})();
|
|
|
|
console.log(`\n${testId}: All ${scenarioCount} encoding scenarios tested ✓`);
|
|
}); |