479 lines
15 KiB
TypeScript
479 lines
15 KiB
TypeScript
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
import { startTestSmtpServer } from '../../helpers/server.loader.js';
|
|
import { createSmtpClient } from '../../helpers/smtp.client.js';
|
|
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
|
|
let testServer: any;
|
|
|
|
tap.test('setup test SMTP server', async () => {
|
|
testServer = await startTestSmtpServer();
|
|
expect(testServer).toBeTruthy();
|
|
expect(testServer.port).toBeGreaterThan(0);
|
|
});
|
|
|
|
tap.test('CEP-08: Basic custom headers', async () => {
|
|
const smtpClient = createSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false,
|
|
connectionTimeout: 5000,
|
|
debug: true
|
|
});
|
|
|
|
await smtpClient.connect();
|
|
|
|
// Create email with custom headers
|
|
const email = new Email({
|
|
from: 'sender@example.com',
|
|
to: ['recipient@example.com'],
|
|
subject: 'Custom Headers Test',
|
|
text: 'Testing custom headers',
|
|
headers: {
|
|
'X-Custom-Header': 'Custom Value',
|
|
'X-Campaign-ID': 'CAMP-2024-03',
|
|
'X-Priority': 'High',
|
|
'X-Mailer': 'Custom SMTP Client v1.0'
|
|
}
|
|
});
|
|
|
|
// Capture sent headers
|
|
const sentHeaders: { [key: string]: string } = {};
|
|
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
|
|
|
|
smtpClient.sendCommand = async (command: string) => {
|
|
if (command.includes(':') && !command.startsWith('MAIL') && !command.startsWith('RCPT')) {
|
|
const [key, ...valueParts] = command.split(':');
|
|
if (key && key.toLowerCase().startsWith('x-')) {
|
|
sentHeaders[key.trim()] = valueParts.join(':').trim();
|
|
}
|
|
}
|
|
return originalSendCommand(command);
|
|
};
|
|
|
|
const result = await smtpClient.sendMail(email);
|
|
expect(result).toBeTruthy();
|
|
|
|
console.log('Custom headers sent:');
|
|
Object.entries(sentHeaders).forEach(([key, value]) => {
|
|
console.log(` ${key}: ${value}`);
|
|
});
|
|
|
|
// Verify custom headers were sent
|
|
expect(Object.keys(sentHeaders).length).toBeGreaterThanOrEqual(4);
|
|
expect(sentHeaders['X-Custom-Header']).toEqual('Custom Value');
|
|
expect(sentHeaders['X-Campaign-ID']).toEqual('CAMP-2024-03');
|
|
|
|
await smtpClient.close();
|
|
});
|
|
|
|
tap.test('CEP-08: Standard headers override protection', async () => {
|
|
const smtpClient = createSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false,
|
|
connectionTimeout: 5000,
|
|
debug: true
|
|
});
|
|
|
|
await smtpClient.connect();
|
|
|
|
// Try to override standard headers via custom headers
|
|
const email = new Email({
|
|
from: 'real-sender@example.com',
|
|
to: ['real-recipient@example.com'],
|
|
subject: 'Real Subject',
|
|
text: 'Testing header override protection',
|
|
headers: {
|
|
'From': 'fake-sender@example.com', // Should not override
|
|
'To': 'fake-recipient@example.com', // Should not override
|
|
'Subject': 'Fake Subject', // Should not override
|
|
'Date': 'Mon, 1 Jan 2000 00:00:00 +0000', // Might be allowed
|
|
'Message-ID': '<fake@example.com>', // Might be allowed
|
|
'X-Original-From': 'tracking@example.com' // Custom header, should work
|
|
}
|
|
});
|
|
|
|
// Capture actual headers
|
|
const actualHeaders: { [key: string]: string } = {};
|
|
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
|
|
|
|
smtpClient.sendCommand = async (command: string) => {
|
|
if (command.includes(':') && !command.startsWith('MAIL') && !command.startsWith('RCPT')) {
|
|
const [key, ...valueParts] = command.split(':');
|
|
const headerKey = key.trim();
|
|
if (['From', 'To', 'Subject', 'Date', 'Message-ID'].includes(headerKey)) {
|
|
actualHeaders[headerKey] = valueParts.join(':').trim();
|
|
}
|
|
}
|
|
return originalSendCommand(command);
|
|
};
|
|
|
|
await smtpClient.sendMail(email);
|
|
|
|
console.log('\nHeader override protection test:');
|
|
console.log('From:', actualHeaders['From']);
|
|
console.log('To:', actualHeaders['To']);
|
|
console.log('Subject:', actualHeaders['Subject']);
|
|
|
|
// Standard headers should not be overridden
|
|
expect(actualHeaders['From']).toInclude('real-sender@example.com');
|
|
expect(actualHeaders['To']).toInclude('real-recipient@example.com');
|
|
expect(actualHeaders['Subject']).toInclude('Real Subject');
|
|
|
|
await smtpClient.close();
|
|
});
|
|
|
|
tap.test('CEP-08: Tracking and analytics headers', async () => {
|
|
const smtpClient = createSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false,
|
|
connectionTimeout: 5000,
|
|
debug: true
|
|
});
|
|
|
|
await smtpClient.connect();
|
|
|
|
// Common tracking headers
|
|
const email = new Email({
|
|
from: 'marketing@example.com',
|
|
to: ['customer@example.com'],
|
|
subject: 'Special Offer Inside!',
|
|
text: 'Check out our special offers',
|
|
headers: {
|
|
'X-Campaign-ID': 'SPRING-2024-SALE',
|
|
'X-Customer-ID': 'CUST-12345',
|
|
'X-Segment': 'high-value-customers',
|
|
'X-AB-Test': 'variant-b',
|
|
'X-Send-Time': new Date().toISOString(),
|
|
'X-Template-Version': '2.1.0',
|
|
'List-Unsubscribe': '<https://example.com/unsubscribe?id=12345>',
|
|
'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click',
|
|
'Precedence': 'bulk'
|
|
}
|
|
});
|
|
|
|
const result = await smtpClient.sendMail(email);
|
|
expect(result).toBeTruthy();
|
|
|
|
console.log('Sent email with tracking headers for analytics');
|
|
|
|
await smtpClient.close();
|
|
});
|
|
|
|
tap.test('CEP-08: MIME extension headers', async () => {
|
|
const smtpClient = createSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false,
|
|
connectionTimeout: 5000,
|
|
debug: true
|
|
});
|
|
|
|
await smtpClient.connect();
|
|
|
|
// MIME-related custom headers
|
|
const email = new Email({
|
|
from: 'sender@example.com',
|
|
to: ['recipient@example.com'],
|
|
subject: 'MIME Extensions Test',
|
|
html: '<p>HTML content</p>',
|
|
text: 'Plain text content',
|
|
headers: {
|
|
'MIME-Version': '1.0', // Usually auto-added
|
|
'X-Accept-Language': 'en-US, en;q=0.9, fr;q=0.8',
|
|
'X-Auto-Response-Suppress': 'DR, RN, NRN, OOF',
|
|
'Importance': 'high',
|
|
'X-Priority': '1',
|
|
'X-MSMail-Priority': 'High',
|
|
'Sensitivity': 'Company-Confidential'
|
|
}
|
|
});
|
|
|
|
// Monitor headers
|
|
const mimeHeaders: string[] = [];
|
|
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
|
|
|
|
smtpClient.sendCommand = async (command: string) => {
|
|
if (command.includes(':') &&
|
|
(command.includes('MIME') ||
|
|
command.includes('Importance') ||
|
|
command.includes('Priority') ||
|
|
command.includes('Sensitivity'))) {
|
|
mimeHeaders.push(command.trim());
|
|
}
|
|
return originalSendCommand(command);
|
|
};
|
|
|
|
await smtpClient.sendMail(email);
|
|
|
|
console.log('\nMIME extension headers:');
|
|
mimeHeaders.forEach(header => console.log(` ${header}`));
|
|
|
|
await smtpClient.close();
|
|
});
|
|
|
|
tap.test('CEP-08: Email threading headers', async () => {
|
|
const smtpClient = createSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false,
|
|
connectionTimeout: 5000,
|
|
debug: true
|
|
});
|
|
|
|
await smtpClient.connect();
|
|
|
|
// Simulate email thread
|
|
const messageId = `<${Date.now()}.${Math.random()}@example.com>`;
|
|
const inReplyTo = '<original-message@example.com>';
|
|
const references = '<thread-start@example.com> <second-message@example.com>';
|
|
|
|
const email = new Email({
|
|
from: 'sender@example.com',
|
|
to: ['recipient@example.com'],
|
|
subject: 'Re: Email Threading Test',
|
|
text: 'This is a reply in the thread',
|
|
headers: {
|
|
'Message-ID': messageId,
|
|
'In-Reply-To': inReplyTo,
|
|
'References': references,
|
|
'Thread-Topic': 'Email Threading Test',
|
|
'Thread-Index': Buffer.from('thread-data').toString('base64')
|
|
}
|
|
});
|
|
|
|
// Capture threading headers
|
|
const threadingHeaders: { [key: string]: string } = {};
|
|
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
|
|
|
|
smtpClient.sendCommand = async (command: string) => {
|
|
const threadHeaders = ['Message-ID', 'In-Reply-To', 'References', 'Thread-Topic', 'Thread-Index'];
|
|
const [key, ...valueParts] = command.split(':');
|
|
if (threadHeaders.includes(key.trim())) {
|
|
threadingHeaders[key.trim()] = valueParts.join(':').trim();
|
|
}
|
|
return originalSendCommand(command);
|
|
};
|
|
|
|
await smtpClient.sendMail(email);
|
|
|
|
console.log('\nThreading headers:');
|
|
Object.entries(threadingHeaders).forEach(([key, value]) => {
|
|
console.log(` ${key}: ${value}`);
|
|
});
|
|
|
|
// Verify threading headers
|
|
expect(threadingHeaders['In-Reply-To']).toEqual(inReplyTo);
|
|
expect(threadingHeaders['References']).toInclude(references);
|
|
|
|
await smtpClient.close();
|
|
});
|
|
|
|
tap.test('CEP-08: Security and authentication headers', async () => {
|
|
const smtpClient = createSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false,
|
|
connectionTimeout: 5000,
|
|
debug: true
|
|
});
|
|
|
|
await smtpClient.connect();
|
|
|
|
// Security-related headers
|
|
const email = new Email({
|
|
from: 'secure@example.com',
|
|
to: ['recipient@example.com'],
|
|
subject: 'Security Headers Test',
|
|
text: 'Testing security headers',
|
|
headers: {
|
|
'X-Originating-IP': '[192.168.1.100]',
|
|
'X-Auth-Result': 'PASS',
|
|
'X-Spam-Score': '0.1',
|
|
'X-Spam-Status': 'No, score=0.1',
|
|
'X-Virus-Scanned': 'ClamAV using ClamSMTP',
|
|
'Authentication-Results': 'example.com; spf=pass smtp.mailfrom=sender@example.com',
|
|
'ARC-Seal': 'i=1; cv=none; d=example.com; s=arc-20240315; t=1710500000;',
|
|
'ARC-Message-Signature': 'i=1; a=rsa-sha256; c=relaxed/relaxed;',
|
|
'ARC-Authentication-Results': 'i=1; example.com; spf=pass'
|
|
}
|
|
});
|
|
|
|
const result = await smtpClient.sendMail(email);
|
|
expect(result).toBeTruthy();
|
|
|
|
console.log('Sent email with security and authentication headers');
|
|
|
|
await smtpClient.close();
|
|
});
|
|
|
|
tap.test('CEP-08: Header folding for long values', async () => {
|
|
const smtpClient = createSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false,
|
|
connectionTimeout: 5000,
|
|
debug: true
|
|
});
|
|
|
|
await smtpClient.connect();
|
|
|
|
// Create headers with long values that need folding
|
|
const longValue = 'This is a very long header value that exceeds the recommended 78 character limit per line and should be folded according to RFC 5322 specifications for proper email transmission';
|
|
|
|
const email = new Email({
|
|
from: 'sender@example.com',
|
|
to: ['recipient@example.com'],
|
|
subject: 'Header Folding Test with a very long subject line that should be properly folded',
|
|
text: 'Testing header folding',
|
|
headers: {
|
|
'X-Long-Header': longValue,
|
|
'X-Multiple-Values': 'value1@example.com, value2@example.com, value3@example.com, value4@example.com, value5@example.com, value6@example.com',
|
|
'References': '<msg1@example.com> <msg2@example.com> <msg3@example.com> <msg4@example.com> <msg5@example.com> <msg6@example.com> <msg7@example.com>'
|
|
}
|
|
});
|
|
|
|
// Monitor line lengths
|
|
let maxLineLength = 0;
|
|
let foldedLines = 0;
|
|
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
|
|
|
|
smtpClient.sendCommand = async (command: string) => {
|
|
const lines = command.split('\r\n');
|
|
lines.forEach(line => {
|
|
const length = line.length;
|
|
maxLineLength = Math.max(maxLineLength, length);
|
|
if (line.startsWith(' ') || line.startsWith('\t')) {
|
|
foldedLines++;
|
|
}
|
|
});
|
|
return originalSendCommand(command);
|
|
};
|
|
|
|
await smtpClient.sendMail(email);
|
|
|
|
console.log(`\nHeader folding results:`);
|
|
console.log(` Maximum line length: ${maxLineLength}`);
|
|
console.log(` Folded continuation lines: ${foldedLines}`);
|
|
|
|
// RFC 5322 recommends 78 chars, requires < 998
|
|
if (maxLineLength > 998) {
|
|
console.log(' WARNING: Line length exceeds RFC 5322 limit');
|
|
}
|
|
|
|
await smtpClient.close();
|
|
});
|
|
|
|
tap.test('CEP-08: Custom headers with special characters', async () => {
|
|
const smtpClient = createSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false,
|
|
connectionTimeout: 5000,
|
|
debug: true
|
|
});
|
|
|
|
await smtpClient.connect();
|
|
|
|
// Headers with special characters
|
|
const email = new Email({
|
|
from: 'sender@example.com',
|
|
to: ['recipient@example.com'],
|
|
subject: 'Special Characters in Headers',
|
|
text: 'Testing special characters',
|
|
headers: {
|
|
'X-Special-Chars': 'Value with special: !@#$%^&*()',
|
|
'X-Quoted-String': '"This is a quoted string"',
|
|
'X-Unicode': 'Unicode: café, naïve, 你好',
|
|
'X-Control-Chars': 'No\ttabs\nor\rnewlines', // Should be sanitized
|
|
'X-Empty': '',
|
|
'X-Spaces': ' trimmed ',
|
|
'X-Semicolon': 'part1; part2; part3'
|
|
}
|
|
});
|
|
|
|
// Capture how special characters are handled
|
|
const specialHeaders: { [key: string]: string } = {};
|
|
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
|
|
|
|
smtpClient.sendCommand = async (command: string) => {
|
|
if (command.toLowerCase().includes('x-') && command.includes(':')) {
|
|
const [key, ...valueParts] = command.split(':');
|
|
specialHeaders[key.trim()] = valueParts.join(':').trim();
|
|
}
|
|
return originalSendCommand(command);
|
|
};
|
|
|
|
await smtpClient.sendMail(email);
|
|
|
|
console.log('\nSpecial character handling:');
|
|
Object.entries(specialHeaders).forEach(([key, value]) => {
|
|
console.log(` ${key}: "${value}"`);
|
|
// Check for proper encoding/escaping
|
|
if (value.includes('=?') && value.includes('?=')) {
|
|
console.log(` -> Encoded as RFC 2047`);
|
|
}
|
|
});
|
|
|
|
await smtpClient.close();
|
|
});
|
|
|
|
tap.test('CEP-08: Duplicate header handling', async () => {
|
|
const smtpClient = createSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false,
|
|
connectionTimeout: 5000,
|
|
debug: true
|
|
});
|
|
|
|
await smtpClient.connect();
|
|
|
|
// Some headers can appear multiple times
|
|
const email = new Email({
|
|
from: 'sender@example.com',
|
|
to: ['recipient@example.com'],
|
|
subject: 'Duplicate Headers Test',
|
|
text: 'Testing duplicate headers',
|
|
headers: {
|
|
'Received': 'from server1.example.com',
|
|
'X-Received': 'from server2.example.com', // Workaround for multiple
|
|
'Comments': 'First comment',
|
|
'X-Comments': 'Second comment', // Workaround for multiple
|
|
'X-Tag': ['tag1', 'tag2', 'tag3'] // Array might create multiple headers
|
|
}
|
|
});
|
|
|
|
// Count occurrences of headers
|
|
const headerCounts: { [key: string]: number } = {};
|
|
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
|
|
|
|
smtpClient.sendCommand = async (command: string) => {
|
|
if (command.includes(':') && !command.startsWith('MAIL') && !command.startsWith('RCPT')) {
|
|
const [key] = command.split(':');
|
|
const headerKey = key.trim();
|
|
headerCounts[headerKey] = (headerCounts[headerKey] || 0) + 1;
|
|
}
|
|
return originalSendCommand(command);
|
|
};
|
|
|
|
await smtpClient.sendMail(email);
|
|
|
|
console.log('\nHeader occurrence counts:');
|
|
Object.entries(headerCounts)
|
|
.filter(([key, count]) => count > 1 || key.includes('Received') || key.includes('Comments'))
|
|
.forEach(([key, count]) => {
|
|
console.log(` ${key}: ${count} occurrence(s)`);
|
|
});
|
|
|
|
await smtpClient.close();
|
|
});
|
|
|
|
tap.test('cleanup test SMTP server', async () => {
|
|
if (testServer) {
|
|
await testServer.stop();
|
|
}
|
|
});
|
|
|
|
export default tap.start(); |