This commit is contained in:
2025-05-24 16:19:19 +00:00
parent 3d669ed9dd
commit 4e4c7df558
50 changed files with 17232 additions and 340 deletions

View File

@ -0,0 +1,245 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
import { Email } from '../../../ts/mail/core/classes.email.js';
let testServer: ITestServer;
let smtpClient: SmtpClient;
tap.test('setup - start SMTP server for email composition tests', async () => {
testServer = await startTestServer({
port: 2570,
tlsEnabled: false,
authRequired: false
});
expect(testServer.port).toEqual(2570);
});
tap.test('setup - create SMTP client', async () => {
smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
const isConnected = await smtpClient.verify();
expect(isConnected).toBeTrue();
});
tap.test('CEP-01: Basic Headers - should send email with required headers', async () => {
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Test Email with Basic Headers',
text: 'This is the plain text body'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
expect(result.acceptedRecipients).toContain('recipient@example.com');
expect(result.messageId).toBeTypeofString();
console.log('✅ Basic email headers sent successfully');
console.log('📧 Message ID:', result.messageId);
});
tap.test('CEP-01: Basic Headers - should handle multiple recipients', async () => {
const email = new Email({
from: 'sender@example.com',
to: ['recipient1@example.com', 'recipient2@example.com', 'recipient3@example.com'],
subject: 'Email to Multiple Recipients',
text: 'This email has multiple recipients'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
expect(result.acceptedRecipients).toContain('recipient1@example.com');
expect(result.acceptedRecipients).toContain('recipient2@example.com');
expect(result.acceptedRecipients).toContain('recipient3@example.com');
console.log(`✅ Sent to ${result.acceptedRecipients.length} recipients`);
});
tap.test('CEP-01: Basic Headers - should support CC and BCC recipients', async () => {
const email = new Email({
from: 'sender@example.com',
to: 'primary@example.com',
cc: ['cc1@example.com', 'cc2@example.com'],
bcc: ['bcc1@example.com', 'bcc2@example.com'],
subject: 'Email with CC and BCC',
text: 'Testing CC and BCC functionality'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
// All recipients should be accepted
expect(result.acceptedRecipients.length).toEqual(5);
console.log('✅ CC and BCC recipients handled correctly');
});
tap.test('CEP-01: Basic Headers - should add custom headers', async () => {
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Email with Custom Headers',
text: 'This email contains custom headers',
headers: {
'X-Custom-Header': 'custom-value',
'X-Priority': '1',
'X-Mailer': 'DCRouter Test Suite',
'Reply-To': 'replies@example.com'
}
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ Custom headers added to email');
});
tap.test('CEP-01: Basic Headers - should set email priority', async () => {
// Test high priority
const highPriorityEmail = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'High Priority Email',
text: 'This is a high priority message',
priority: 'high'
});
const highResult = await smtpClient.sendMail(highPriorityEmail);
expect(highResult.success).toBeTrue();
// Test normal priority
const normalPriorityEmail = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Normal Priority Email',
text: 'This is a normal priority message',
priority: 'normal'
});
const normalResult = await smtpClient.sendMail(normalPriorityEmail);
expect(normalResult.success).toBeTrue();
// Test low priority
const lowPriorityEmail = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Low Priority Email',
text: 'This is a low priority message',
priority: 'low'
});
const lowResult = await smtpClient.sendMail(lowPriorityEmail);
expect(lowResult.success).toBeTrue();
console.log('✅ All priority levels handled correctly');
});
tap.test('CEP-01: Basic Headers - should handle sender with display name', async () => {
const email = new Email({
from: 'John Doe <john.doe@example.com>',
to: 'Jane Smith <jane.smith@example.com>',
subject: 'Email with Display Names',
text: 'Testing display names in email addresses'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
expect(result.envelope?.from).toContain('john.doe@example.com');
console.log('✅ Display names in addresses handled correctly');
});
tap.test('CEP-01: Basic Headers - should generate proper Message-ID', async () => {
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Message-ID Test',
text: 'Testing Message-ID generation'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
expect(result.messageId).toBeTypeofString();
// Message-ID should be in format <id@domain>
expect(result.messageId).toMatch(/^<.+@.+>$/);
console.log('✅ Valid Message-ID generated:', result.messageId);
});
tap.test('CEP-01: Basic Headers - should handle long subject lines', async () => {
const longSubject = 'This is a very long subject line that exceeds the typical length and might need to be wrapped according to RFC specifications for email headers';
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: longSubject,
text: 'Email with long subject line'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ Long subject line handled correctly');
});
tap.test('CEP-01: Basic Headers - should sanitize header values', async () => {
// Test with potentially problematic characters
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Subject with\nnewline and\rcarriage return',
text: 'Testing header sanitization',
headers: {
'X-Test-Header': 'Value with\nnewline'
}
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ Header values sanitized correctly');
});
tap.test('CEP-01: Basic Headers - should include Date header', async () => {
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Date Header Test',
text: 'Testing automatic Date header'
});
const beforeSend = new Date();
const result = await smtpClient.sendMail(email);
const afterSend = new Date();
expect(result.success).toBeTrue();
// The email should have been sent between beforeSend and afterSend
console.log('✅ Date header automatically included');
});
tap.test('cleanup - close SMTP client', async () => {
if (smtpClient && smtpClient.isConnected()) {
await smtpClient.close();
}
});
tap.test('cleanup - stop SMTP server', async () => {
await stopTestServer(testServer);
});
tap.start();

View File

@ -0,0 +1,321 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
import { Email } from '../../../ts/mail/core/classes.email.js';
let testServer: ITestServer;
let smtpClient: SmtpClient;
tap.test('setup - start SMTP server for MIME tests', async () => {
testServer = await startTestServer({
port: 2571,
tlsEnabled: false,
authRequired: false,
size: 25 * 1024 * 1024 // 25MB for attachment tests
});
expect(testServer.port).toEqual(2571);
});
tap.test('setup - create SMTP client', async () => {
smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
socketTimeout: 60000, // Longer timeout for large attachments
debug: true
});
const isConnected = await smtpClient.verify();
expect(isConnected).toBeTrue();
});
tap.test('CEP-02: MIME Multipart - should send multipart/alternative (text + HTML)', async () => {
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Multipart Alternative Test',
text: 'This is the plain text version of the email.',
html: '<html><body><h1>HTML Version</h1><p>This is the <strong>HTML version</strong> of the email.</p></body></html>'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ Multipart/alternative email sent successfully');
});
tap.test('CEP-02: MIME Multipart - should send multipart/mixed with attachments', async () => {
const textAttachment = Buffer.from('This is a text file attachment content.');
const csvData = 'Name,Email,Score\nJohn Doe,john@example.com,95\nJane Smith,jane@example.com,87';
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Multipart Mixed with Attachments',
text: 'This email contains attachments.',
html: '<p>This email contains <strong>attachments</strong>.</p>',
attachments: [
{
filename: 'document.txt',
content: textAttachment,
contentType: 'text/plain'
},
{
filename: 'data.csv',
content: Buffer.from(csvData),
contentType: 'text/csv'
}
]
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ Multipart/mixed with attachments sent successfully');
});
tap.test('CEP-02: MIME Multipart - should handle inline images', async () => {
// Create a small test image (1x1 red pixel PNG)
const redPixelPng = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==',
'base64'
);
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Inline Image Test',
text: 'This email contains an inline image.',
html: '<p>Here is an inline image: <img src="cid:red-pixel" alt="Red Pixel"></p>',
attachments: [
{
filename: 'red-pixel.png',
content: redPixelPng,
contentType: 'image/png',
contentId: 'red-pixel' // Content-ID for inline reference
}
]
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ Email with inline image sent successfully');
});
tap.test('CEP-02: MIME Multipart - should handle multiple attachment types', async () => {
const attachments = [
{
filename: 'text.txt',
content: Buffer.from('Plain text file'),
contentType: 'text/plain'
},
{
filename: 'data.json',
content: Buffer.from(JSON.stringify({ test: 'data', value: 123 })),
contentType: 'application/json'
},
{
filename: 'binary.bin',
content: Buffer.from([0x00, 0x01, 0x02, 0x03, 0xFF, 0xFE, 0xFD]),
contentType: 'application/octet-stream'
},
{
filename: 'document.pdf',
content: Buffer.from('%PDF-1.4\n%fake pdf content for testing'),
contentType: 'application/pdf'
}
];
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Multiple Attachment Types',
text: 'Testing various attachment types',
attachments
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ Multiple attachment types handled correctly');
});
tap.test('CEP-02: MIME Multipart - should encode binary attachments with base64', async () => {
// Create binary data with all byte values
const binaryData = Buffer.alloc(256);
for (let i = 0; i < 256; i++) {
binaryData[i] = i;
}
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Binary Attachment Encoding Test',
text: 'This email contains binary data that must be base64 encoded',
attachments: [
{
filename: 'binary-data.bin',
content: binaryData,
contentType: 'application/octet-stream',
encoding: 'base64' // Explicitly specify encoding
}
]
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ Binary attachment base64 encoded correctly');
});
tap.test('CEP-02: MIME Multipart - should handle large attachments', async () => {
// Create a 5MB attachment
const largeData = Buffer.alloc(5 * 1024 * 1024);
for (let i = 0; i < largeData.length; i++) {
largeData[i] = i % 256;
}
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Large Attachment Test',
text: 'This email contains a large attachment',
attachments: [
{
filename: 'large-file.dat',
content: largeData,
contentType: 'application/octet-stream'
}
]
});
const startTime = Date.now();
const result = await smtpClient.sendMail(email);
const duration = Date.now() - startTime;
expect(result.success).toBeTrue();
console.log(`✅ Large attachment (5MB) sent in ${duration}ms`);
});
tap.test('CEP-02: MIME Multipart - should handle nested multipart structures', async () => {
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Complex Multipart Structure',
text: 'Plain text version',
html: '<p>HTML version with <img src="cid:logo" alt="Logo"></p>',
attachments: [
{
filename: 'logo.png',
content: Buffer.from('fake png data'),
contentType: 'image/png',
contentId: 'logo' // Inline image
},
{
filename: 'attachment.txt',
content: Buffer.from('Regular attachment'),
contentType: 'text/plain'
}
]
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ Nested multipart structure (mixed + related + alternative) handled');
});
tap.test('CEP-02: MIME Multipart - should handle attachment filenames with special characters', async () => {
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Special Filename Test',
text: 'Testing attachments with special filenames',
attachments: [
{
filename: 'file with spaces.txt',
content: Buffer.from('Content 1'),
contentType: 'text/plain'
},
{
filename: 'файл.txt', // Cyrillic
content: Buffer.from('Content 2'),
contentType: 'text/plain'
},
{
filename: '文件.txt', // Chinese
content: Buffer.from('Content 3'),
contentType: 'text/plain'
}
]
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ Special characters in filenames handled correctly');
});
tap.test('CEP-02: MIME Multipart - should handle empty attachments', async () => {
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Empty Attachment Test',
text: 'This email has an empty attachment',
attachments: [
{
filename: 'empty.txt',
content: Buffer.from(''), // Empty content
contentType: 'text/plain'
}
]
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ Empty attachment handled correctly');
});
tap.test('CEP-02: MIME Multipart - should respect content-type parameters', async () => {
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Content-Type Parameters Test',
text: 'Testing content-type with charset',
html: '<p>HTML with specific charset</p>',
attachments: [
{
filename: 'utf8-text.txt',
content: Buffer.from('UTF-8 text: 你好世界'),
contentType: 'text/plain; charset=utf-8'
},
{
filename: 'data.xml',
content: Buffer.from('<?xml version="1.0" encoding="UTF-8"?><root>Test</root>'),
contentType: 'application/xml; charset=utf-8'
}
]
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ Content-type parameters preserved correctly');
});
tap.test('cleanup - close SMTP client', async () => {
if (smtpClient && smtpClient.isConnected()) {
await smtpClient.close();
}
});
tap.test('cleanup - stop SMTP server', async () => {
await stopTestServer(testServer);
});
tap.start();

View File

@ -0,0 +1,334 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
import { Email } from '../../../ts/mail/core/classes.email.js';
import * as crypto from 'crypto';
let testServer: ITestServer;
let smtpClient: SmtpClient;
tap.test('setup - start SMTP server for attachment encoding tests', async () => {
testServer = await startTestServer({
port: 2572,
tlsEnabled: false,
authRequired: false,
size: 50 * 1024 * 1024 // 50MB for large attachment tests
});
expect(testServer.port).toEqual(2572);
});
tap.test('setup - create SMTP client', async () => {
smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
socketTimeout: 120000, // 2 minutes for large attachments
debug: true
});
const isConnected = await smtpClient.verify();
expect(isConnected).toBeTrue();
});
tap.test('CEP-03: Attachment Encoding - should encode text attachment with base64', async () => {
const textContent = 'This is a test text file.\nIt contains multiple lines.\nAnd some special characters: © ® ™';
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Text Attachment Base64 Test',
text: 'Email with text attachment',
attachments: [{
filename: 'test.txt',
content: Buffer.from(textContent),
contentType: 'text/plain',
encoding: 'base64'
}]
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ Text attachment encoded with base64');
});
tap.test('CEP-03: Attachment Encoding - should encode binary data correctly', async () => {
// Create binary data with all possible byte values
const binaryData = Buffer.alloc(256);
for (let i = 0; i < 256; i++) {
binaryData[i] = i;
}
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Binary Attachment Test',
text: 'Email with binary attachment',
attachments: [{
filename: 'binary.dat',
content: binaryData,
contentType: 'application/octet-stream'
}]
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ Binary data encoded correctly');
});
tap.test('CEP-03: Attachment Encoding - should handle various file types', async () => {
const attachments = [
{
filename: 'image.jpg',
content: Buffer.from('/9j/4AAQSkZJRgABAQEASABIAAD/2wBD', 'base64'), // Partial JPEG header
contentType: 'image/jpeg'
},
{
filename: 'document.pdf',
content: Buffer.from('%PDF-1.4\n%âÃÏÓ\n', 'utf8'),
contentType: 'application/pdf'
},
{
filename: 'archive.zip',
content: Buffer.from('PK\x03\x04'), // ZIP magic number
contentType: 'application/zip'
},
{
filename: 'audio.mp3',
content: Buffer.from('ID3'), // MP3 ID3 tag
contentType: 'audio/mpeg'
}
];
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Multiple File Types Test',
text: 'Testing various attachment types',
attachments
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ Various file types encoded correctly');
});
tap.test('CEP-03: Attachment Encoding - should handle quoted-printable encoding', async () => {
const textWithSpecialChars = 'This line has special chars: café, naïve, résumé\r\nThis line is very long and might need soft line breaks when encoded with quoted-printable encoding method\r\n=This line starts with equals sign';
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Quoted-Printable Test',
text: 'Email with quoted-printable attachment',
attachments: [{
filename: 'special-chars.txt',
content: Buffer.from(textWithSpecialChars, 'utf8'),
contentType: 'text/plain; charset=utf-8',
encoding: 'quoted-printable'
}]
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ Quoted-printable encoding handled correctly');
});
tap.test('CEP-03: Attachment Encoding - should handle content-disposition', async () => {
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Content-Disposition Test',
text: 'Testing attachment vs inline disposition',
html: '<p>Image below: <img src="cid:inline-image"></p>',
attachments: [
{
filename: 'attachment.txt',
content: Buffer.from('This is an attachment'),
contentType: 'text/plain'
// Default disposition is 'attachment'
},
{
filename: 'inline-image.png',
content: Buffer.from('fake png data'),
contentType: 'image/png',
contentId: 'inline-image' // Makes it inline
}
]
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ Content-disposition handled correctly');
});
tap.test('CEP-03: Attachment Encoding - should handle large attachments efficiently', async () => {
// Create a 10MB attachment
const largeSize = 10 * 1024 * 1024;
const largeData = crypto.randomBytes(largeSize);
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Large Attachment Test',
text: 'Email with large attachment',
attachments: [{
filename: 'large-file.bin',
content: largeData,
contentType: 'application/octet-stream'
}]
});
const startTime = Date.now();
const result = await smtpClient.sendMail(email);
const duration = Date.now() - startTime;
expect(result.success).toBeTrue();
console.log(`✅ Large attachment (${largeSize / 1024 / 1024}MB) sent in ${duration}ms`);
console.log(` Throughput: ${(largeSize / 1024 / 1024 / (duration / 1000)).toFixed(2)} MB/s`);
});
tap.test('CEP-03: Attachment Encoding - should handle Unicode filenames', async () => {
const unicodeAttachments = [
{
filename: '文档.txt', // Chinese
content: Buffer.from('Chinese filename test'),
contentType: 'text/plain'
},
{
filename: 'файл.txt', // Russian
content: Buffer.from('Russian filename test'),
contentType: 'text/plain'
},
{
filename: 'ファイル.txt', // Japanese
content: Buffer.from('Japanese filename test'),
contentType: 'text/plain'
},
{
filename: '🎉emoji🎊.txt', // Emoji
content: Buffer.from('Emoji filename test'),
contentType: 'text/plain'
}
];
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Unicode Filenames Test',
text: 'Testing Unicode characters in filenames',
attachments: unicodeAttachments
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ Unicode filenames encoded correctly');
});
tap.test('CEP-03: Attachment Encoding - should handle special MIME headers', async () => {
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'MIME Headers Test',
text: 'Testing special MIME headers',
attachments: [{
filename: 'report.xml',
content: Buffer.from('<?xml version="1.0"?><root>test</root>'),
contentType: 'application/xml; charset=utf-8',
encoding: 'base64',
headers: {
'Content-Description': 'Monthly Report',
'Content-Transfer-Encoding': 'base64',
'Content-ID': '<report-2024-01@example.com>'
}
}]
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ Special MIME headers handled correctly');
});
tap.test('CEP-03: Attachment Encoding - should handle attachment size limits', async () => {
// Test with attachment near server limit
const nearLimitSize = 45 * 1024 * 1024; // 45MB (near 50MB limit)
const nearLimitData = Buffer.alloc(nearLimitSize);
// Fill with some pattern to avoid compression benefits
for (let i = 0; i < nearLimitSize; i++) {
nearLimitData[i] = i % 256;
}
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Near Size Limit Test',
text: 'Testing attachment near size limit',
attachments: [{
filename: 'near-limit.bin',
content: nearLimitData,
contentType: 'application/octet-stream'
}]
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log(`✅ Attachment near size limit (${nearLimitSize / 1024 / 1024}MB) accepted`);
});
tap.test('CEP-03: Attachment Encoding - should handle mixed encoding types', async () => {
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Mixed Encoding Test',
text: 'Plain text body',
html: '<p>HTML body with special chars: café</p>',
attachments: [
{
filename: 'base64.bin',
content: crypto.randomBytes(1024),
contentType: 'application/octet-stream',
encoding: 'base64'
},
{
filename: 'quoted.txt',
content: Buffer.from('Text with special chars: naïve café résumé'),
contentType: 'text/plain; charset=utf-8',
encoding: 'quoted-printable'
},
{
filename: '7bit.txt',
content: Buffer.from('Simple ASCII text only'),
contentType: 'text/plain',
encoding: '7bit'
}
]
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ Mixed encoding types handled correctly');
});
tap.test('cleanup - close SMTP client', async () => {
if (smtpClient && smtpClient.isConnected()) {
await smtpClient.close();
}
});
tap.test('cleanup - stop SMTP server', async () => {
await stopTestServer(testServer);
});
tap.start();

View File

@ -0,0 +1,398 @@
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-04: Basic BCC handling', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Create email with BCC recipients
const email = new Email({
from: 'sender@example.com',
to: ['visible@example.com'],
cc: ['copied@example.com'],
bcc: ['hidden1@example.com', 'hidden2@example.com'],
subject: 'Test BCC Handling',
text: 'This message has BCC recipients'
});
// Send the email
const result = await smtpClient.sendMail(email);
expect(result).toBeTruthy();
expect(result.accepted).toBeArray();
// All recipients (including BCC) should be accepted
const totalRecipients = [...email.to, ...email.cc, ...email.bcc];
expect(result.accepted.length).toEqual(totalRecipients.length);
console.log('BCC recipients processed:', email.bcc.length);
console.log('Total recipients:', totalRecipients.length);
await smtpClient.close();
});
tap.test('CEP-04: BCC header exclusion', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Create email with BCC
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
bcc: ['secret@example.com'],
subject: 'BCC Header Test',
text: 'Testing BCC header exclusion'
});
// Monitor the actual SMTP commands
let dataContent = '';
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
let inDataPhase = false;
smtpClient.sendCommand = async (command: string) => {
if (command === 'DATA') {
inDataPhase = true;
} else if (inDataPhase && command === '.') {
inDataPhase = false;
} else if (inDataPhase) {
dataContent += command + '\n';
}
return originalSendCommand(command);
};
await smtpClient.sendMail(email);
// Verify BCC header is not in the message
expect(dataContent.toLowerCase()).not.toInclude('bcc:');
console.log('Verified: BCC header not included in message data');
// Verify other headers are present
expect(dataContent.toLowerCase()).toInclude('to:');
expect(dataContent.toLowerCase()).toInclude('from:');
expect(dataContent.toLowerCase()).toInclude('subject:');
await smtpClient.close();
});
tap.test('CEP-04: Large BCC list handling', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 10000,
debug: true
});
await smtpClient.connect();
// Create email with many BCC recipients
const bccCount = 50;
const bccRecipients = Array.from({ length: bccCount },
(_, i) => `bcc${i + 1}@example.com`
);
const email = new Email({
from: 'sender@example.com',
to: ['visible@example.com'],
bcc: bccRecipients,
subject: 'Large BCC List Test',
text: `This message has ${bccCount} BCC recipients`
});
console.log(`Sending email with ${bccCount} BCC recipients...`);
const startTime = Date.now();
const result = await smtpClient.sendMail(email);
const elapsed = Date.now() - startTime;
expect(result).toBeTruthy();
expect(result.accepted).toBeArray();
// All BCC recipients should be processed
expect(result.accepted).toIncludeAllMembers(bccRecipients);
console.log(`Processed ${bccCount} BCC recipients in ${elapsed}ms`);
console.log(`Average time per recipient: ${(elapsed / bccCount).toFixed(2)}ms`);
await smtpClient.close();
});
tap.test('CEP-04: BCC-only email', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Create email with only BCC recipients (no TO or CC)
const email = new Email({
from: 'sender@example.com',
bcc: ['hidden1@example.com', 'hidden2@example.com', 'hidden3@example.com'],
subject: 'BCC-Only Email',
text: 'This email has only BCC recipients'
});
const result = await smtpClient.sendMail(email);
expect(result).toBeTruthy();
expect(result.accepted.length).toEqual(email.bcc.length);
console.log('Successfully sent BCC-only email to', email.bcc.length, 'recipients');
// Verify the email has appropriate headers
let hasToHeader = false;
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
if (command.toLowerCase().includes('to:')) {
hasToHeader = true;
}
return originalSendCommand(command);
};
// Send another BCC-only email to check headers
await smtpClient.sendMail(new Email({
from: 'sender@example.com',
bcc: ['test@example.com'],
subject: 'Header Check',
text: 'Checking headers'
}));
// Some implementations add "To: undisclosed-recipients:;" for BCC-only emails
console.log('Email has TO header:', hasToHeader);
await smtpClient.close();
});
tap.test('CEP-04: Mixed recipient types', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Create email with all recipient types
const email = new Email({
from: 'sender@example.com',
to: ['to1@example.com', 'to2@example.com'],
cc: ['cc1@example.com', 'cc2@example.com', 'cc3@example.com'],
bcc: ['bcc1@example.com', 'bcc2@example.com', 'bcc3@example.com', 'bcc4@example.com'],
subject: 'Mixed Recipients Test',
text: 'Testing all recipient types together'
});
// Track RCPT TO commands
const rcptCommands: string[] = [];
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
if (command.startsWith('RCPT TO:')) {
rcptCommands.push(command);
}
return originalSendCommand(command);
};
const result = await smtpClient.sendMail(email);
// Verify all recipients received RCPT TO
const totalExpected = email.to.length + email.cc.length + email.bcc.length;
expect(rcptCommands.length).toEqual(totalExpected);
console.log('Recipient breakdown:');
console.log(` TO: ${email.to.length} recipients`);
console.log(` CC: ${email.cc.length} recipients`);
console.log(` BCC: ${email.bcc.length} recipients`);
console.log(` Total RCPT TO commands: ${rcptCommands.length}`);
// Verify each recipient type
for (const recipient of email.to) {
expect(rcptCommands).toIncludeAnyMembers([`RCPT TO:<${recipient}>`]);
}
for (const recipient of email.cc) {
expect(rcptCommands).toIncludeAnyMembers([`RCPT TO:<${recipient}>`]);
}
for (const recipient of email.bcc) {
expect(rcptCommands).toIncludeAnyMembers([`RCPT TO:<${recipient}>`]);
}
await smtpClient.close();
});
tap.test('CEP-04: BCC with special characters', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// BCC addresses with special characters
const specialBccAddresses = [
'user+tag@example.com',
'first.last@example.com',
'user_name@example.com',
'"quoted string"@example.com',
'user@sub.domain.example.com'
];
const email = new Email({
from: 'sender@example.com',
to: ['visible@example.com'],
bcc: specialBccAddresses,
subject: 'BCC Special Characters Test',
text: 'Testing BCC with special character addresses'
});
const result = await smtpClient.sendMail(email);
expect(result).toBeTruthy();
console.log('BCC addresses with special characters processed:');
specialBccAddresses.forEach((addr, i) => {
const accepted = result.accepted.includes(addr);
console.log(` ${i + 1}. ${addr} - ${accepted ? 'Accepted' : 'Rejected'}`);
});
await smtpClient.close();
});
tap.test('CEP-04: BCC duplicate handling', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Create email with duplicate addresses across recipient types
const email = new Email({
from: 'sender@example.com',
to: ['shared@example.com', 'unique1@example.com'],
cc: ['shared@example.com', 'unique2@example.com'],
bcc: ['shared@example.com', 'unique3@example.com', 'unique3@example.com'], // Duplicate in BCC
subject: 'Duplicate Recipients Test',
text: 'Testing duplicate handling across recipient types'
});
// Track unique RCPT TO commands
const rcptSet = new Set<string>();
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
if (command.startsWith('RCPT TO:')) {
rcptSet.add(command);
}
return originalSendCommand(command);
};
const result = await smtpClient.sendMail(email);
console.log('Duplicate handling results:');
console.log(` Total addresses provided: ${email.to.length + email.cc.length + email.bcc.length}`);
console.log(` Unique RCPT TO commands: ${rcptSet.size}`);
console.log(` Duplicates detected: ${(email.to.length + email.cc.length + email.bcc.length) - rcptSet.size}`);
// The client should handle duplicates appropriately
expect(rcptSet.size).toBeLessThanOrEqual(email.to.length + email.cc.length + email.bcc.length);
await smtpClient.close();
});
tap.test('CEP-04: BCC performance impact', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 10000,
debug: false // Quiet for performance test
});
await smtpClient.connect();
// Test performance with different BCC counts
const bccCounts = [0, 10, 25, 50];
const results: { count: number; time: number }[] = [];
for (const count of bccCounts) {
const bccRecipients = Array.from({ length: count },
(_, i) => `bcc${i}@example.com`
);
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
bcc: bccRecipients,
subject: `Performance Test - ${count} BCCs`,
text: 'Performance testing'
});
const startTime = Date.now();
await smtpClient.sendMail(email);
const elapsed = Date.now() - startTime;
results.push({ count, time: elapsed });
}
console.log('\nBCC Performance Impact:');
console.log('BCC Count | Time (ms) | Per-recipient (ms)');
console.log('----------|-----------|-------------------');
results.forEach(r => {
const perRecipient = r.count > 0 ? (r.time / r.count).toFixed(2) : 'N/A';
console.log(`${r.count.toString().padEnd(9)} | ${r.time.toString().padEnd(9)} | ${perRecipient}`);
});
// Performance should scale linearly with BCC count
if (results.length >= 2) {
const timeIncrease = results[results.length - 1].time - results[0].time;
const countIncrease = results[results.length - 1].count - results[0].count;
const msPerBcc = countIncrease > 0 ? timeIncrease / countIncrease : 0;
console.log(`\nAverage time per BCC recipient: ${msPerBcc.toFixed(2)}ms`);
}
await smtpClient.close();
});
tap.test('cleanup test SMTP server', async () => {
if (testServer) {
await testServer.stop();
}
});
export default tap.start();

View File

@ -0,0 +1,499 @@
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-05: Basic Reply-To header', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Create email with Reply-To
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
replyTo: 'replies@example.com',
subject: 'Test Reply-To Header',
text: 'Please reply to the Reply-To address'
});
// Monitor headers
let hasReplyTo = false;
let replyToValue = '';
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
if (command.toLowerCase().includes('reply-to:')) {
hasReplyTo = true;
replyToValue = command.split(':')[1]?.trim() || '';
}
return originalSendCommand(command);
};
const result = await smtpClient.sendMail(email);
expect(result).toBeTruthy();
expect(hasReplyTo).toBeTruthy();
expect(replyToValue).toInclude('replies@example.com');
console.log('Reply-To header added:', replyToValue);
await smtpClient.close();
});
tap.test('CEP-05: Multiple Reply-To addresses', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Create email with multiple Reply-To addresses
const replyToAddresses = [
'support@example.com',
'help@example.com',
'feedback@example.com'
];
const email = new Email({
from: 'noreply@example.com',
to: ['user@example.com'],
replyTo: replyToAddresses,
subject: 'Multiple Reply-To Test',
text: 'Testing multiple reply-to addresses'
});
// Capture the Reply-To header
let capturedReplyTo = '';
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
if (command.toLowerCase().includes('reply-to:')) {
capturedReplyTo = command;
}
return originalSendCommand(command);
};
await smtpClient.sendMail(email);
console.log('Multiple Reply-To header:', capturedReplyTo);
// Verify all addresses are included
replyToAddresses.forEach(addr => {
expect(capturedReplyTo).toInclude(addr);
});
await smtpClient.close();
});
tap.test('CEP-05: Return-Path handling', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Create email with custom return path
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
returnPath: 'bounces@example.com',
subject: 'Test Return-Path',
text: 'Testing return path handling'
});
// Monitor MAIL FROM command (sets return path)
let mailFromAddress = '';
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
if (command.startsWith('MAIL FROM:')) {
const match = command.match(/MAIL FROM:<([^>]+)>/);
if (match) {
mailFromAddress = match[1];
}
}
return originalSendCommand(command);
};
await smtpClient.sendMail(email);
// Return-Path should be set in MAIL FROM
expect(mailFromAddress).toEqual('bounces@example.com');
console.log('Return-Path set via MAIL FROM:', mailFromAddress);
await smtpClient.close();
});
tap.test('CEP-05: Reply-To with display names', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Test various Reply-To formats with display names
const replyToFormats = [
'Support Team <support@example.com>',
'"Customer Service" <service@example.com>',
'help@example.com (Help Desk)',
'<noreply@example.com>'
];
for (const replyTo of replyToFormats) {
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
replyTo: replyTo,
subject: 'Reply-To Format Test',
text: `Testing Reply-To format: ${replyTo}`
});
let capturedReplyTo = '';
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
if (command.toLowerCase().includes('reply-to:')) {
capturedReplyTo = command;
}
return originalSendCommand(command);
};
await smtpClient.sendMail(email);
console.log(`\nReply-To format: ${replyTo}`);
console.log(`Sent as: ${capturedReplyTo.trim()}`);
}
await smtpClient.close();
});
tap.test('CEP-05: Return-Path vs From address', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Test different scenarios
const scenarios = [
{
name: 'No return path specified',
from: 'sender@example.com',
returnPath: undefined,
expectedMailFrom: 'sender@example.com'
},
{
name: 'Different return path',
from: 'noreply@example.com',
returnPath: 'bounces@example.com',
expectedMailFrom: 'bounces@example.com'
},
{
name: 'Empty return path (null sender)',
from: 'system@example.com',
returnPath: '',
expectedMailFrom: ''
}
];
for (const scenario of scenarios) {
console.log(`\nTesting: ${scenario.name}`);
const email = new Email({
from: scenario.from,
to: ['test@example.com'],
returnPath: scenario.returnPath,
subject: scenario.name,
text: 'Testing return path scenarios'
});
let mailFromAddress: string | null = null;
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
if (command.startsWith('MAIL FROM:')) {
const match = command.match(/MAIL FROM:<([^>]*)>/);
if (match) {
mailFromAddress = match[1];
}
}
return originalSendCommand(command);
};
await smtpClient.sendMail(email);
console.log(` From: ${scenario.from}`);
console.log(` Return-Path: ${scenario.returnPath === undefined ? '(not set)' : scenario.returnPath || '(empty)'}`);
console.log(` MAIL FROM: <${mailFromAddress}>`);
expect(mailFromAddress).toEqual(scenario.expectedMailFrom);
}
await smtpClient.close();
});
tap.test('CEP-05: Reply-To interaction with From', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Test Reply-To same as From
const email1 = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
replyTo: 'sender@example.com', // Same as From
subject: 'Reply-To Same as From',
text: 'Testing when Reply-To equals From'
});
let headers1: string[] = [];
const originalSendCommand1 = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
if (command.includes(':') && !command.startsWith('MAIL') && !command.startsWith('RCPT')) {
headers1.push(command);
}
return originalSendCommand1(command);
};
await smtpClient.sendMail(email1);
// Some implementations might omit Reply-To when it's the same as From
const hasReplyTo1 = headers1.some(h => h.toLowerCase().includes('reply-to:'));
console.log('Reply-To same as From - header included:', hasReplyTo1);
// Test Reply-To different from From
const email2 = new Email({
from: 'noreply@example.com',
to: ['recipient@example.com'],
replyTo: 'support@example.com', // Different from From
subject: 'Reply-To Different from From',
text: 'Testing when Reply-To differs from From'
});
let headers2: string[] = [];
smtpClient.sendCommand = async (command: string) => {
if (command.includes(':') && !command.startsWith('MAIL') && !command.startsWith('RCPT')) {
headers2.push(command);
}
return originalSendCommand1(command);
};
await smtpClient.sendMail(email2);
// Reply-To should definitely be included when different
const hasReplyTo2 = headers2.some(h => h.toLowerCase().includes('reply-to:'));
expect(hasReplyTo2).toBeTruthy();
console.log('Reply-To different from From - header included:', hasReplyTo2);
await smtpClient.close();
});
tap.test('CEP-05: Special Reply-To addresses', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Test special Reply-To addresses
const specialCases = [
{
name: 'Group syntax',
replyTo: 'Support Team:support@example.com,help@example.com;'
},
{
name: 'Quoted local part',
replyTo: '"support team"@example.com'
},
{
name: 'International domain',
replyTo: 'info@例え.jp'
},
{
name: 'Plus addressing',
replyTo: 'support+urgent@example.com'
}
];
for (const testCase of specialCases) {
console.log(`\nTesting ${testCase.name}: ${testCase.replyTo}`);
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
replyTo: testCase.replyTo,
subject: `Special Reply-To: ${testCase.name}`,
text: 'Testing special Reply-To address formats'
});
try {
const result = await smtpClient.sendMail(email);
console.log(` Result: ${result ? 'Success' : 'Failed'}`);
} catch (error) {
console.log(` Error: ${error.message}`);
}
}
await smtpClient.close();
});
tap.test('CEP-05: Return-Path with VERP', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Variable Envelope Return Path (VERP) for bounce handling
const recipients = [
'user1@example.com',
'user2@example.com',
'user3@example.com'
];
for (const recipient of recipients) {
// Create VERP address
const recipientId = recipient.replace('@', '=');
const verpAddress = `bounces+${recipientId}@example.com`;
const email = new Email({
from: 'newsletter@example.com',
to: [recipient],
returnPath: verpAddress,
subject: 'Newsletter with VERP',
text: 'This email uses VERP for bounce tracking'
});
let capturedMailFrom = '';
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
if (command.startsWith('MAIL FROM:')) {
capturedMailFrom = command;
}
return originalSendCommand(command);
};
await smtpClient.sendMail(email);
console.log(`\nRecipient: ${recipient}`);
console.log(`VERP Return-Path: ${verpAddress}`);
console.log(`MAIL FROM: ${capturedMailFrom}`);
expect(capturedMailFrom).toInclude(verpAddress);
}
await smtpClient.close();
});
tap.test('CEP-05: Header precedence and conflicts', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Test potential conflicts
const email = new Email({
from: 'From Name <from@example.com>',
to: ['recipient@example.com'],
replyTo: ['reply1@example.com', 'reply2@example.com'],
returnPath: 'bounces@example.com',
headers: {
'Reply-To': 'override@example.com', // Try to override
'Return-Path': 'override-bounces@example.com' // Try to override
},
subject: 'Header Precedence Test',
text: 'Testing header precedence and conflicts'
});
let capturedHeaders: { [key: string]: string } = {};
let mailFromAddress = '';
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
if (command.startsWith('MAIL FROM:')) {
const match = command.match(/MAIL FROM:<([^>]*)>/);
if (match) {
mailFromAddress = match[1];
}
} else if (command.includes(':') && !command.startsWith('RCPT')) {
const [key, ...valueParts] = command.split(':');
if (key) {
capturedHeaders[key.toLowerCase().trim()] = valueParts.join(':').trim();
}
}
return originalSendCommand(command);
};
await smtpClient.sendMail(email);
console.log('\nHeader precedence results:');
console.log('Reply-To header:', capturedHeaders['reply-to'] || 'not found');
console.log('MAIL FROM (Return-Path):', mailFromAddress);
// The Email class properties should take precedence over raw headers
expect(capturedHeaders['reply-to']).toInclude('reply1@example.com');
expect(mailFromAddress).toEqual('bounces@example.com');
await smtpClient.close();
});
tap.test('cleanup test SMTP server', async () => {
if (testServer) {
await testServer.stop();
}
});
export default tap.start();

View File

@ -0,0 +1,462 @@
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({
features: ['8BITMIME', 'SMTPUTF8'] // Enable UTF-8 support
});
expect(testServer).toBeTruthy();
expect(testServer.port).toBeGreaterThan(0);
});
tap.test('CEP-06: Basic UTF-8 content', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Create email with UTF-8 content
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'UTF-8 Test: こんにちは 🌍',
text: 'Hello in multiple languages:\n' +
'English: Hello World\n' +
'Japanese: こんにちは世界\n' +
'Chinese: 你好世界\n' +
'Arabic: مرحبا بالعالم\n' +
'Russian: Привет мир\n' +
'Emoji: 🌍🌎🌏✉️📧'
});
// Check content encoding
let contentType = '';
let charset = '';
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
if (command.toLowerCase().includes('content-type:')) {
contentType = command;
const charsetMatch = command.match(/charset=([^;\s]+)/i);
if (charsetMatch) {
charset = charsetMatch[1];
}
}
return originalSendCommand(command);
};
const result = await smtpClient.sendMail(email);
expect(result).toBeTruthy();
console.log('Content-Type:', contentType.trim());
console.log('Charset:', charset || 'not specified');
// Should use UTF-8 charset
expect(charset.toLowerCase()).toMatch(/utf-?8/);
await smtpClient.close();
});
tap.test('CEP-06: International email addresses', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Check if server supports SMTPUTF8
const ehloResponse = await smtpClient.sendCommand('EHLO testclient.example.com');
const supportsSmtpUtf8 = ehloResponse.includes('SMTPUTF8');
console.log('Server supports SMTPUTF8:', supportsSmtpUtf8);
// Test international email addresses
const internationalAddresses = [
'user@例え.jp',
'utilisateur@exemple.fr',
'benutzer@beispiel.de',
'пользователь@пример.рф',
'用户@例子.中国'
];
for (const address of internationalAddresses) {
console.log(`\nTesting international address: ${address}`);
const email = new Email({
from: 'sender@example.com',
to: [address],
subject: 'International Address Test',
text: `Testing delivery to: ${address}`
});
try {
// Monitor MAIL FROM with SMTPUTF8
let smtpUtf8Used = false;
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
if (command.includes('SMTPUTF8')) {
smtpUtf8Used = true;
}
return originalSendCommand(command);
};
const result = await smtpClient.sendMail(email);
console.log(` Result: ${result ? 'Success' : 'Failed'}`);
console.log(` SMTPUTF8 used: ${smtpUtf8Used}`);
if (!supportsSmtpUtf8 && !result) {
console.log(' Expected failure - server does not support SMTPUTF8');
}
} catch (error) {
console.log(` Error: ${error.message}`);
if (!supportsSmtpUtf8) {
console.log(' Expected - server does not support international addresses');
}
}
}
await smtpClient.close();
});
tap.test('CEP-06: UTF-8 in headers', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Create email with UTF-8 in various headers
const email = new Email({
from: '"发件人" <sender@example.com>',
to: ['"收件人" <recipient@example.com>'],
subject: 'Meeting: Café ☕ at 3pm 🕒',
headers: {
'X-Custom-Header': 'Custom UTF-8: αβγδε',
'X-Language': '日本語'
},
text: 'Meeting at the café to discuss the project.'
});
// Capture encoded headers
const capturedHeaders: string[] = [];
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
if (command.includes(':') && !command.startsWith('MAIL') && !command.startsWith('RCPT')) {
capturedHeaders.push(command);
}
return originalSendCommand(command);
};
await smtpClient.sendMail(email);
console.log('\nCaptured headers with UTF-8:');
capturedHeaders.forEach(header => {
// Check for encoded-word syntax (RFC 2047)
if (header.includes('=?')) {
const encodedMatch = header.match(/=\?([^?]+)\?([BQ])\?([^?]+)\?=/);
if (encodedMatch) {
console.log(` Encoded header: ${header.substring(0, 50)}...`);
console.log(` Charset: ${encodedMatch[1]}, Encoding: ${encodedMatch[2]}`);
}
} else if (/[\u0080-\uFFFF]/.test(header)) {
console.log(` Raw UTF-8 header: ${header.substring(0, 50)}...`);
}
});
await smtpClient.close();
});
tap.test('CEP-06: Different character encodings', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Test different encoding scenarios
const encodingTests = [
{
name: 'Plain ASCII',
subject: 'Simple ASCII Subject',
text: 'This is plain ASCII text.',
expectedEncoding: 'none'
},
{
name: 'Latin-1 characters',
subject: 'Café, naïve, résumé',
text: 'Text with Latin-1: àáâãäåæçèéêë',
expectedEncoding: 'quoted-printable or base64'
},
{
name: 'CJK characters',
subject: '会議の予定:明日',
text: '明日の会議は午後3時からです。',
expectedEncoding: 'base64'
},
{
name: 'Mixed scripts',
subject: 'Hello 你好 مرحبا',
text: 'Mixed: English, 中文, العربية, Русский',
expectedEncoding: 'base64'
},
{
name: 'Emoji heavy',
subject: '🎉 Party Time 🎊',
text: '🌟✨🎈🎁🎂🍰🎵🎶💃🕺',
expectedEncoding: 'base64'
}
];
for (const test of encodingTests) {
console.log(`\nTesting: ${test.name}`);
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: test.subject,
text: test.text
});
let transferEncoding = '';
let subjectEncoding = '';
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
if (command.toLowerCase().includes('content-transfer-encoding:')) {
transferEncoding = command.split(':')[1].trim();
}
if (command.toLowerCase().startsWith('subject:')) {
if (command.includes('=?')) {
subjectEncoding = 'encoded-word';
} else {
subjectEncoding = 'raw';
}
}
return originalSendCommand(command);
};
await smtpClient.sendMail(email);
console.log(` Subject encoding: ${subjectEncoding}`);
console.log(` Body transfer encoding: ${transferEncoding}`);
console.log(` Expected: ${test.expectedEncoding}`);
}
await smtpClient.close();
});
tap.test('CEP-06: Line length handling for UTF-8', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Create long lines with UTF-8 characters
const longJapanese = '日本語のテキスト'.repeat(20); // ~300 bytes
const longEmoji = '😀😃😄😁😆😅😂🤣'.repeat(25); // ~800 bytes
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Long UTF-8 Lines Test',
text: `Short line\n${longJapanese}\nAnother short line\n${longEmoji}\nEnd`
});
// Monitor line lengths
let maxLineLength = 0;
let longLines = 0;
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
let inData = false;
smtpClient.sendCommand = async (command: string) => {
if (command === 'DATA') {
inData = true;
} else if (command === '.') {
inData = false;
} else if (inData) {
const lines = command.split('\r\n');
lines.forEach(line => {
const byteLength = Buffer.byteLength(line, 'utf8');
maxLineLength = Math.max(maxLineLength, byteLength);
if (byteLength > 78) { // RFC recommended line length
longLines++;
}
});
}
return originalSendCommand(command);
};
await smtpClient.sendMail(email);
console.log(`\nLine length analysis:`);
console.log(` Maximum line length: ${maxLineLength} bytes`);
console.log(` Lines over 78 bytes: ${longLines}`);
// Lines should be properly wrapped or encoded
if (maxLineLength > 998) { // RFC hard limit
console.log(' WARNING: Lines exceed RFC 5321 limit of 998 bytes');
}
await smtpClient.close();
});
tap.test('CEP-06: Bidirectional text handling', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Test bidirectional text (RTL and LTR mixed)
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'مرحبا Hello שלום',
text: 'Mixed direction text:\n' +
'English text followed by عربي ثم עברית\n' +
'מספרים: 123 أرقام: ٤٥٦\n' +
'LTR: Hello → RTL: مرحبا ← LTR: World'
});
const result = await smtpClient.sendMail(email);
expect(result).toBeTruthy();
console.log('Successfully sent email with bidirectional text');
await smtpClient.close();
});
tap.test('CEP-06: Special UTF-8 cases', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Test special UTF-8 cases
const specialCases = [
{
name: 'Zero-width characters',
text: 'VisibleZeroWidthNonJoinerBetweenWords'
},
{
name: 'Combining characters',
text: 'a\u0300 e\u0301 i\u0302 o\u0303 u\u0308' // à é î õ ü
},
{
name: 'Surrogate pairs',
text: '𝐇𝐞𝐥𝐥𝐨 𝕎𝕠𝕣𝕝𝕕 🏴󠁧󠁢󠁳󠁣󠁴󠁿' // Mathematical bold, flags
},
{
name: 'Right-to-left marks',
text: '\u202Edetrevni si txet sihT\u202C' // RTL override
},
{
name: 'Non-standard spaces',
text: 'Different spaces: \u2000\u2001\u2002\u2003\u2004'
}
];
for (const testCase of specialCases) {
console.log(`\nTesting ${testCase.name}`);
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: `UTF-8 Special: ${testCase.name}`,
text: testCase.text
});
try {
const result = await smtpClient.sendMail(email);
console.log(` Result: ${result ? 'Success' : 'Failed'}`);
console.log(` Text bytes: ${Buffer.byteLength(testCase.text, 'utf8')}`);
} catch (error) {
console.log(` Error: ${error.message}`);
}
}
await smtpClient.close();
});
tap.test('CEP-06: Fallback encoding for non-UTF8 servers', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
preferredEncoding: 'quoted-printable', // Force specific encoding
debug: true
});
await smtpClient.connect();
// Send UTF-8 content that needs encoding
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Fallback Encoding: Café français',
text: 'Testing encoding: àèìòù ÀÈÌÒÙ äëïöü ñç'
});
let encodingUsed = '';
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
if (command.toLowerCase().includes('content-transfer-encoding:')) {
encodingUsed = command.split(':')[1].trim();
}
return originalSendCommand(command);
};
await smtpClient.sendMail(email);
console.log('\nFallback encoding test:');
console.log('Preferred encoding:', 'quoted-printable');
console.log('Actual encoding used:', encodingUsed);
await smtpClient.close();
});
tap.test('cleanup test SMTP server', async () => {
if (testServer) {
await testServer.stop();
}
});
export default tap.start();

View File

@ -0,0 +1,635 @@
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-07: Basic HTML email', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Create HTML email
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'HTML Email Test',
html: `
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; }
.header { color: #333; background: #f0f0f0; padding: 20px; }
.content { padding: 20px; }
.footer { color: #666; font-size: 12px; }
</style>
</head>
<body>
<div class="header">
<h1>Welcome!</h1>
</div>
<div class="content">
<p>This is an <strong>HTML email</strong> with <em>formatting</em>.</p>
<ul>
<li>Feature 1</li>
<li>Feature 2</li>
<li>Feature 3</li>
</ul>
</div>
<div class="footer">
<p>© 2024 Example Corp</p>
</div>
</body>
</html>
`,
text: 'Welcome! This is an HTML email with formatting. Features: 1, 2, 3. © 2024 Example Corp'
});
// Monitor content type
let contentType = '';
let boundary = '';
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
if (command.toLowerCase().includes('content-type:')) {
contentType = command;
const boundaryMatch = command.match(/boundary="?([^";\s]+)"?/i);
if (boundaryMatch) {
boundary = boundaryMatch[1];
}
}
return originalSendCommand(command);
};
const result = await smtpClient.sendMail(email);
expect(result).toBeTruthy();
console.log('Content-Type:', contentType.trim());
console.log('Multipart boundary:', boundary || 'not found');
// Should be multipart/alternative for HTML+text
expect(contentType.toLowerCase()).toInclude('multipart');
await smtpClient.close();
});
tap.test('CEP-07: HTML email with inline images', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 10000,
debug: true
});
await smtpClient.connect();
// Create a simple 1x1 red pixel PNG
const redPixelBase64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==';
// Create HTML email with inline image
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Email with Inline Images',
html: `
<html>
<body>
<h1>Email with Inline Images</h1>
<p>Here's an inline image:</p>
<img src="cid:image001" alt="Red pixel" width="100" height="100">
<p>And here's another one:</p>
<img src="cid:logo" alt="Company logo">
</body>
</html>
`,
attachments: [
{
filename: 'red-pixel.png',
content: Buffer.from(redPixelBase64, 'base64'),
contentType: 'image/png',
cid: 'image001' // Content-ID for inline reference
},
{
filename: 'logo.png',
content: Buffer.from(redPixelBase64, 'base64'), // Reuse for demo
contentType: 'image/png',
cid: 'logo'
}
]
});
// Monitor multipart structure
let multipartType = '';
let partCount = 0;
let hasContentId = false;
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
if (command.toLowerCase().includes('content-type:')) {
if (command.toLowerCase().includes('multipart/related')) {
multipartType = 'related';
} else if (command.toLowerCase().includes('multipart/mixed')) {
multipartType = 'mixed';
}
if (command.includes('--')) {
partCount++;
}
}
if (command.toLowerCase().includes('content-id:')) {
hasContentId = true;
}
return originalSendCommand(command);
};
const result = await smtpClient.sendMail(email);
expect(result).toBeTruthy();
console.log('Multipart type:', multipartType);
console.log('Has Content-ID headers:', hasContentId);
// Should use multipart/related for inline images
expect(multipartType).toEqual('related');
expect(hasContentId).toBeTruthy();
await smtpClient.close();
});
tap.test('CEP-07: Complex HTML with multiple inline resources', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 10000,
debug: true
});
await smtpClient.connect();
// Create email with multiple inline resources
const email = new Email({
from: 'newsletter@example.com',
to: ['subscriber@example.com'],
subject: 'Newsletter with Rich Content',
html: `
<html>
<head>
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 0; }
.header { background: url('cid:header-bg') center/cover; height: 200px; }
.logo { width: 150px; }
.product { display: inline-block; margin: 10px; }
.product img { width: 100px; height: 100px; }
</style>
</head>
<body>
<div class="header">
<img src="cid:logo" alt="Company Logo" class="logo">
</div>
<h1>Monthly Newsletter</h1>
<div class="products">
<div class="product">
<img src="cid:product1" alt="Product 1">
<p>Product 1</p>
</div>
<div class="product">
<img src="cid:product2" alt="Product 2">
<p>Product 2</p>
</div>
<div class="product">
<img src="cid:product3" alt="Product 3">
<p>Product 3</p>
</div>
</div>
<img src="cid:footer-divider" alt="" style="width: 100%; height: 2px;">
<p>© 2024 Example Corp</p>
</body>
</html>
`,
text: 'Monthly Newsletter - View in HTML for best experience',
attachments: [
{
filename: 'header-bg.jpg',
content: Buffer.from('fake-image-data'),
contentType: 'image/jpeg',
cid: 'header-bg'
},
{
filename: 'logo.png',
content: Buffer.from('fake-logo-data'),
contentType: 'image/png',
cid: 'logo'
},
{
filename: 'product1.jpg',
content: Buffer.from('fake-product1-data'),
contentType: 'image/jpeg',
cid: 'product1'
},
{
filename: 'product2.jpg',
content: Buffer.from('fake-product2-data'),
contentType: 'image/jpeg',
cid: 'product2'
},
{
filename: 'product3.jpg',
content: Buffer.from('fake-product3-data'),
contentType: 'image/jpeg',
cid: 'product3'
},
{
filename: 'divider.gif',
content: Buffer.from('fake-divider-data'),
contentType: 'image/gif',
cid: 'footer-divider'
}
]
});
// Count inline attachments
let inlineAttachments = 0;
let contentIds: string[] = [];
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
if (command.toLowerCase().includes('content-disposition: inline')) {
inlineAttachments++;
}
if (command.toLowerCase().includes('content-id:')) {
const cidMatch = command.match(/content-id:\s*<([^>]+)>/i);
if (cidMatch) {
contentIds.push(cidMatch[1]);
}
}
return originalSendCommand(command);
};
const result = await smtpClient.sendMail(email);
expect(result).toBeTruthy();
console.log(`Inline attachments: ${inlineAttachments}`);
console.log(`Content-IDs found: ${contentIds.length}`);
console.log('CIDs:', contentIds);
// Should have all inline attachments
expect(contentIds.length).toEqual(6);
await smtpClient.close();
});
tap.test('CEP-07: HTML with external and inline images mixed', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Mix of inline and external images
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Mixed Image Sources',
html: `
<html>
<body>
<h1>Mixed Image Sources</h1>
<h2>Inline Image:</h2>
<img src="cid:inline-logo" alt="Inline Logo" width="100">
<h2>External Images:</h2>
<img src="https://via.placeholder.com/150" alt="External Image 1">
<img src="http://example.com/image.jpg" alt="External Image 2">
<h2>Data URI Image:</h2>
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==" alt="Data URI">
</body>
</html>
`,
attachments: [
{
filename: 'logo.png',
content: Buffer.from('logo-data'),
contentType: 'image/png',
cid: 'inline-logo'
}
]
});
const result = await smtpClient.sendMail(email);
expect(result).toBeTruthy();
console.log('Successfully sent email with mixed image sources');
await smtpClient.close();
});
tap.test('CEP-07: HTML email responsive design', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Responsive HTML email
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Responsive HTML Email',
html: `
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
@media screen and (max-width: 600px) {
.container { width: 100% !important; }
.column { width: 100% !important; display: block !important; }
.mobile-hide { display: none !important; }
}
.container { width: 600px; margin: 0 auto; }
.column { width: 48%; display: inline-block; vertical-align: top; }
img { max-width: 100%; height: auto; }
</style>
</head>
<body>
<div class="container">
<h1>Responsive Design Test</h1>
<div class="column">
<img src="cid:left-image" alt="Left Column">
<p>Left column content</p>
</div>
<div class="column">
<img src="cid:right-image" alt="Right Column">
<p>Right column content</p>
</div>
<p class="mobile-hide">This text is hidden on mobile devices</p>
</div>
</body>
</html>
`,
text: 'Responsive Design Test - View in HTML',
attachments: [
{
filename: 'left.jpg',
content: Buffer.from('left-image-data'),
contentType: 'image/jpeg',
cid: 'left-image'
},
{
filename: 'right.jpg',
content: Buffer.from('right-image-data'),
contentType: 'image/jpeg',
cid: 'right-image'
}
]
});
const result = await smtpClient.sendMail(email);
expect(result).toBeTruthy();
console.log('Successfully sent responsive HTML email');
await smtpClient.close();
});
tap.test('CEP-07: HTML sanitization and security', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Email with potentially dangerous HTML
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'HTML Security Test',
html: `
<html>
<body>
<h1>Security Test</h1>
<!-- Scripts should be handled safely -->
<script>alert('This should not execute');</script>
<img src="x" onerror="alert('XSS')">
<a href="javascript:alert('Click')">Dangerous Link</a>
<iframe src="https://evil.com"></iframe>
<form action="https://evil.com/steal">
<input type="text" name="data">
</form>
<!-- Safe content -->
<p>This is safe text content.</p>
<img src="cid:safe-image" alt="Safe Image">
</body>
</html>
`,
text: 'Security Test - Plain text version',
attachments: [
{
filename: 'safe.png',
content: Buffer.from('safe-image-data'),
contentType: 'image/png',
cid: 'safe-image'
}
]
});
// Note: The Email class should handle dangerous content appropriately
const result = await smtpClient.sendMail(email);
expect(result).toBeTruthy();
console.log('Sent email with potentially dangerous HTML (should be handled by Email class)');
await smtpClient.close();
});
tap.test('CEP-07: Large HTML email with many inline images', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 30000,
debug: false // Quiet for performance
});
await smtpClient.connect();
// Create email with many inline images
const imageCount = 20;
const attachments: any[] = [];
let htmlContent = '<html><body><h1>Performance Test</h1>';
for (let i = 0; i < imageCount; i++) {
const cid = `image${i}`;
htmlContent += `<img src="cid:${cid}" alt="Image ${i}" width="50" height="50">`;
attachments.push({
filename: `image${i}.png`,
content: Buffer.from(`fake-image-data-${i}`),
contentType: 'image/png',
cid: cid
});
}
htmlContent += '</body></html>';
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: `Email with ${imageCount} inline images`,
html: htmlContent,
attachments: attachments
});
console.log(`Sending email with ${imageCount} inline images...`);
const startTime = Date.now();
const result = await smtpClient.sendMail(email);
const elapsed = Date.now() - startTime;
expect(result).toBeTruthy();
console.log(`Sent in ${elapsed}ms (${(elapsed/imageCount).toFixed(2)}ms per image)`);
await smtpClient.close();
});
tap.test('CEP-07: Alternative content for non-HTML clients', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Email with rich HTML and good plain text alternative
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Newsletter - March 2024',
html: `
<html>
<body style="font-family: Arial, sans-serif;">
<div style="background: #f0f0f0; padding: 20px;">
<img src="cid:header" alt="Company Newsletter" style="width: 100%; max-width: 600px;">
</div>
<div style="padding: 20px;">
<h1 style="color: #333;">March Newsletter</h1>
<h2 style="color: #666;">Featured Articles</h2>
<ul>
<li><a href="https://example.com/article1">10 Tips for Spring Cleaning</a></li>
<li><a href="https://example.com/article2">New Product Launch</a></li>
<li><a href="https://example.com/article3">Customer Success Story</a></li>
</ul>
<div style="background: #e0e0e0; padding: 15px; margin: 20px 0;">
<h3>Special Offer!</h3>
<p>Get 20% off with code: <strong>SPRING20</strong></p>
<img src="cid:offer" alt="Special Offer" style="width: 100%; max-width: 400px;">
</div>
</div>
<div style="background: #333; color: #fff; padding: 20px; text-align: center;">
<p>© 2024 Example Corp | <a href="https://example.com/unsubscribe" style="color: #fff;">Unsubscribe</a></p>
</div>
</body>
</html>
`,
text: `COMPANY NEWSLETTER
March 2024
FEATURED ARTICLES
* 10 Tips for Spring Cleaning
https://example.com/article1
* New Product Launch
https://example.com/article2
* Customer Success Story
https://example.com/article3
SPECIAL OFFER!
Get 20% off with code: SPRING20
---
© 2024 Example Corp
Unsubscribe: https://example.com/unsubscribe`,
attachments: [
{
filename: 'header.jpg',
content: Buffer.from('header-image'),
contentType: 'image/jpeg',
cid: 'header'
},
{
filename: 'offer.jpg',
content: Buffer.from('offer-image'),
contentType: 'image/jpeg',
cid: 'offer'
}
]
});
// Check multipart/alternative structure
let hasAlternative = false;
let hasTextPart = false;
let hasHtmlPart = false;
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
if (command.toLowerCase().includes('content-type: multipart/alternative')) {
hasAlternative = true;
}
if (command.toLowerCase().includes('content-type: text/plain')) {
hasTextPart = true;
}
if (command.toLowerCase().includes('content-type: text/html')) {
hasHtmlPart = true;
}
return originalSendCommand(command);
};
const result = await smtpClient.sendMail(email);
expect(result).toBeTruthy();
console.log('Multipart/alternative:', hasAlternative);
console.log('Has text part:', hasTextPart);
console.log('Has HTML part:', hasHtmlPart);
// Should have both versions
expect(hasTextPart).toBeTruthy();
expect(hasHtmlPart).toBeTruthy();
await smtpClient.close();
});
tap.test('cleanup test SMTP server', async () => {
if (testServer) {
await testServer.stop();
}
});
export default tap.start();

View File

@ -0,0 +1,479 @@
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();

View File

@ -0,0 +1,497 @@
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-09: Basic priority headers', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Test different priority levels
const priorityLevels = [
{ priority: 'high', headers: { 'X-Priority': '1', 'Importance': 'high' } },
{ priority: 'normal', headers: { 'X-Priority': '3', 'Importance': 'normal' } },
{ priority: 'low', headers: { 'X-Priority': '5', 'Importance': 'low' } }
];
for (const level of priorityLevels) {
console.log(`\nTesting ${level.priority} priority email...`);
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: `${level.priority.toUpperCase()} Priority Test`,
text: `This is a ${level.priority} priority message`,
priority: level.priority as 'high' | 'normal' | 'low'
});
// Monitor 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, value] = command.split(':').map(s => s.trim());
if (key === 'X-Priority' || key === 'Importance' || key === 'X-MSMail-Priority') {
sentHeaders[key] = value;
}
}
return originalSendCommand(command);
};
const result = await smtpClient.sendMail(email);
expect(result).toBeTruthy();
console.log('Priority headers sent:');
Object.entries(sentHeaders).forEach(([key, value]) => {
console.log(` ${key}: ${value}`);
});
}
await smtpClient.close();
});
tap.test('CEP-09: Multiple priority header formats', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Test various priority header combinations
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Multiple Priority Headers Test',
text: 'Testing various priority header formats',
headers: {
'X-Priority': '1 (Highest)',
'X-MSMail-Priority': 'High',
'Importance': 'high',
'Priority': 'urgent',
'X-Message-Flag': 'Follow up'
}
});
// Capture all priority-related headers
const priorityHeaders: string[] = [];
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
const priorityKeywords = ['priority', 'importance', 'urgent', 'flag'];
if (command.includes(':') && priorityKeywords.some(kw => command.toLowerCase().includes(kw))) {
priorityHeaders.push(command.trim());
}
return originalSendCommand(command);
};
await smtpClient.sendMail(email);
console.log('\nAll priority-related headers:');
priorityHeaders.forEach(header => {
console.log(` ${header}`);
});
await smtpClient.close();
});
tap.test('CEP-09: Client-specific priority mappings', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Test how priority maps to different email clients
const clientMappings = [
{
client: 'Outlook',
high: { 'X-Priority': '1', 'X-MSMail-Priority': 'High', 'Importance': 'High' },
normal: { 'X-Priority': '3', 'X-MSMail-Priority': 'Normal', 'Importance': 'Normal' },
low: { 'X-Priority': '5', 'X-MSMail-Priority': 'Low', 'Importance': 'Low' }
},
{
client: 'Thunderbird',
high: { 'X-Priority': '1', 'Importance': 'High' },
normal: { 'X-Priority': '3', 'Importance': 'Normal' },
low: { 'X-Priority': '5', 'Importance': 'Low' }
},
{
client: 'Apple Mail',
high: { 'X-Priority': '1' },
normal: { 'X-Priority': '3' },
low: { 'X-Priority': '5' }
}
];
console.log('\nClient-specific priority header mappings:');
for (const mapping of clientMappings) {
console.log(`\n${mapping.client}:`);
console.log(' High priority:', JSON.stringify(mapping.high));
console.log(' Normal priority:', JSON.stringify(mapping.normal));
console.log(' Low priority:', JSON.stringify(mapping.low));
}
// Send test email with comprehensive priority headers
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Cross-client Priority Test',
text: 'This should appear as high priority in all clients',
priority: 'high'
});
await smtpClient.sendMail(email);
await smtpClient.close();
});
tap.test('CEP-09: Sensitivity and confidentiality headers', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Test sensitivity levels
const sensitivityLevels = [
{ level: 'Personal', description: 'Personal information' },
{ level: 'Private', description: 'Private communication' },
{ level: 'Company-Confidential', description: 'Internal use only' },
{ level: 'Normal', description: 'No special handling' }
];
for (const sensitivity of sensitivityLevels) {
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: `${sensitivity.level} Message`,
text: sensitivity.description,
headers: {
'Sensitivity': sensitivity.level,
'X-Sensitivity': sensitivity.level
}
});
// Monitor sensitivity headers
let sensitivityHeader = '';
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
if (command.toLowerCase().includes('sensitivity:')) {
sensitivityHeader = command.trim();
}
return originalSendCommand(command);
};
await smtpClient.sendMail(email);
console.log(`\nSensitivity: ${sensitivity.level}`);
console.log(` Header sent: ${sensitivityHeader}`);
}
await smtpClient.close();
});
tap.test('CEP-09: Auto-response suppression headers', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Headers to suppress auto-responses (vacation messages, etc.)
const email = new Email({
from: 'noreply@example.com',
to: ['recipient@example.com'],
subject: 'Automated Notification',
text: 'This is an automated message. Please do not reply.',
headers: {
'X-Auto-Response-Suppress': 'All', // Microsoft
'Auto-Submitted': 'auto-generated', // RFC 3834
'Precedence': 'bulk', // Traditional
'X-Autoreply': 'no',
'X-Autorespond': 'no',
'List-Id': '<notifications.example.com>', // Mailing list header
'List-Unsubscribe': '<mailto:unsubscribe@example.com>'
}
});
// Capture auto-response suppression headers
const suppressionHeaders: string[] = [];
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
const suppressionKeywords = ['auto', 'precedence', 'list-', 'bulk'];
if (command.includes(':') && suppressionKeywords.some(kw => command.toLowerCase().includes(kw))) {
suppressionHeaders.push(command.trim());
}
return originalSendCommand(command);
};
await smtpClient.sendMail(email);
console.log('\nAuto-response suppression headers:');
suppressionHeaders.forEach(header => {
console.log(` ${header}`);
});
await smtpClient.close();
});
tap.test('CEP-09: Expiration and retention headers', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Set expiration date for the email
const expirationDate = new Date();
expirationDate.setDate(expirationDate.getDate() + 7); // Expires in 7 days
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Time-sensitive Information',
text: 'This information expires in 7 days',
headers: {
'Expiry-Date': expirationDate.toUTCString(),
'X-Message-TTL': '604800', // 7 days in seconds
'X-Auto-Delete-After': expirationDate.toISOString(),
'X-Retention-Date': expirationDate.toISOString()
}
});
// Monitor expiration headers
const expirationHeaders: { [key: string]: string } = {};
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
if (command.includes(':')) {
const [key, value] = command.split(':').map(s => s.trim());
if (key.toLowerCase().includes('expir') || key.toLowerCase().includes('retention') ||
key.toLowerCase().includes('ttl') || key.toLowerCase().includes('delete')) {
expirationHeaders[key] = value;
}
}
return originalSendCommand(command);
};
await smtpClient.sendMail(email);
console.log('\nExpiration and retention headers:');
Object.entries(expirationHeaders).forEach(([key, value]) => {
console.log(` ${key}: ${value}`);
});
await smtpClient.close();
});
tap.test('CEP-09: Message flags and categories', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Test various message flags and categories
const flaggedEmails = [
{
flag: 'Follow up',
category: 'Action Required',
color: 'red'
},
{
flag: 'For Your Information',
category: 'Informational',
color: 'blue'
},
{
flag: 'Review',
category: 'Pending Review',
color: 'yellow'
}
];
for (const flaggedEmail of flaggedEmails) {
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: `${flaggedEmail.flag}: Important Document`,
text: `This email is flagged as: ${flaggedEmail.flag}`,
headers: {
'X-Message-Flag': flaggedEmail.flag,
'X-Category': flaggedEmail.category,
'X-Color-Label': flaggedEmail.color,
'Keywords': flaggedEmail.flag.replace(' ', '-')
}
});
console.log(`\nSending flagged email: ${flaggedEmail.flag}`);
const result = await smtpClient.sendMail(email);
expect(result).toBeTruthy();
}
await smtpClient.close();
});
tap.test('CEP-09: Priority with delivery timing', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Test deferred delivery with priority
const futureDate = new Date();
futureDate.setHours(futureDate.getHours() + 2); // Deliver in 2 hours
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Scheduled High Priority Message',
text: 'This high priority message should be delivered at a specific time',
priority: 'high',
headers: {
'Deferred-Delivery': futureDate.toUTCString(),
'X-Delay-Until': futureDate.toISOString(),
'X-Priority': '1',
'Importance': 'High'
}
});
// Monitor timing headers
let deferredDeliveryHeader = '';
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
if (command.toLowerCase().includes('defer') || command.toLowerCase().includes('delay')) {
deferredDeliveryHeader = command.trim();
}
return originalSendCommand(command);
};
await smtpClient.sendMail(email);
console.log('\nScheduled delivery with priority:');
console.log(` Priority: High`);
console.log(` Scheduled for: ${futureDate.toUTCString()}`);
if (deferredDeliveryHeader) {
console.log(` Header sent: ${deferredDeliveryHeader}`);
}
await smtpClient.close();
});
tap.test('CEP-09: Priority impact on routing', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Test batch of emails with different priorities
const emails = [
{ priority: 'high', subject: 'URGENT: Server Down', delay: 0 },
{ priority: 'high', subject: 'Critical Security Update', delay: 0 },
{ priority: 'normal', subject: 'Weekly Report', delay: 100 },
{ priority: 'low', subject: 'Newsletter', delay: 200 },
{ priority: 'low', subject: 'Promotional Offer', delay: 200 }
];
console.log('\nSending emails with different priorities:');
const sendTimes: { priority: string; time: number }[] = [];
for (const emailData of emails) {
// Simulate priority-based delays
if (emailData.delay > 0) {
await new Promise(resolve => setTimeout(resolve, emailData.delay));
}
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: emailData.subject,
text: `Priority: ${emailData.priority}`,
priority: emailData.priority as 'high' | 'normal' | 'low'
});
const startTime = Date.now();
await smtpClient.sendMail(email);
const sendTime = Date.now() - startTime;
sendTimes.push({ priority: emailData.priority, time: sendTime });
console.log(` ${emailData.priority.padEnd(6)} - ${emailData.subject} (${sendTime}ms)`);
}
// Analyze send times by priority
const avgByPriority = ['high', 'normal', 'low'].map(priority => {
const times = sendTimes.filter(st => st.priority === priority);
const avg = times.reduce((sum, st) => sum + st.time, 0) / times.length;
return { priority, avg };
});
console.log('\nAverage send time by priority:');
avgByPriority.forEach(({ priority, avg }) => {
console.log(` ${priority}: ${avg.toFixed(2)}ms`);
});
await smtpClient.close();
});
tap.test('cleanup test SMTP server', async () => {
if (testServer) {
await testServer.stop();
}
});
export default tap.start();

View File

@ -0,0 +1,573 @@
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({
features: ['DSN'] // Enable DSN support
});
expect(testServer).toBeTruthy();
expect(testServer.port).toBeGreaterThan(0);
});
tap.test('CEP-10: Read receipt headers', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Create email requesting read receipt
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Important: Please confirm receipt',
text: 'Please confirm you have read this message',
headers: {
'Disposition-Notification-To': 'sender@example.com',
'Return-Receipt-To': 'sender@example.com',
'X-Confirm-Reading-To': 'sender@example.com',
'X-MS-Receipt-Request': 'sender@example.com'
}
});
// Monitor receipt headers
const receiptHeaders: string[] = [];
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
if (command.includes(':') &&
(command.toLowerCase().includes('receipt') ||
command.toLowerCase().includes('notification') ||
command.toLowerCase().includes('confirm'))) {
receiptHeaders.push(command.trim());
}
return originalSendCommand(command);
};
const result = await smtpClient.sendMail(email);
expect(result).toBeTruthy();
console.log('Read receipt headers sent:');
receiptHeaders.forEach(header => {
console.log(` ${header}`);
});
await smtpClient.close();
});
tap.test('CEP-10: DSN (Delivery Status Notification) requests', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Check if server supports DSN
const ehloResponse = await smtpClient.sendCommand('EHLO testclient.example.com');
const supportsDSN = ehloResponse.includes('DSN');
console.log(`Server DSN support: ${supportsDSN}`);
// Create email with DSN options
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'DSN Test Email',
text: 'Testing delivery status notifications',
dsn: {
notify: ['SUCCESS', 'FAILURE', 'DELAY'],
returnType: 'HEADERS',
envid: `msg-${Date.now()}`
}
});
// Monitor DSN parameters in SMTP commands
let mailFromDSN = '';
let rcptToDSN = '';
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
if (command.startsWith('MAIL FROM')) {
mailFromDSN = command;
} else if (command.startsWith('RCPT TO')) {
rcptToDSN = command;
}
return originalSendCommand(command);
};
await smtpClient.sendMail(email);
console.log('\nDSN parameters in SMTP commands:');
console.log('MAIL FROM command:', mailFromDSN);
console.log('RCPT TO command:', rcptToDSN);
// Check for DSN parameters
if (mailFromDSN.includes('ENVID=')) {
console.log(' ✓ ENVID parameter included');
}
if (rcptToDSN.includes('NOTIFY=')) {
console.log(' ✓ NOTIFY parameter included');
}
if (mailFromDSN.includes('RET=')) {
console.log(' ✓ RET parameter included');
}
await smtpClient.close();
});
tap.test('CEP-10: DSN notify options', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Test different DSN notify combinations
const notifyOptions = [
{ notify: ['SUCCESS'], description: 'Notify on successful delivery only' },
{ notify: ['FAILURE'], description: 'Notify on failure only' },
{ notify: ['DELAY'], description: 'Notify on delays only' },
{ notify: ['SUCCESS', 'FAILURE'], description: 'Notify on success and failure' },
{ notify: ['NEVER'], description: 'Never send notifications' },
{ notify: [], description: 'Default notification behavior' }
];
for (const option of notifyOptions) {
console.log(`\nTesting DSN: ${option.description}`);
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: `DSN Test: ${option.description}`,
text: 'Testing DSN notify options',
dsn: {
notify: option.notify as any,
returnType: 'HEADERS'
}
});
// Monitor RCPT TO command
let rcptCommand = '';
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
if (command.startsWith('RCPT TO')) {
rcptCommand = command;
}
return originalSendCommand(command);
};
await smtpClient.sendMail(email);
// Extract NOTIFY parameter
const notifyMatch = rcptCommand.match(/NOTIFY=([A-Z,]+)/);
if (notifyMatch) {
console.log(` NOTIFY parameter: ${notifyMatch[1]}`);
} else {
console.log(' No NOTIFY parameter (default behavior)');
}
}
await smtpClient.close();
});
tap.test('CEP-10: DSN return types', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Test different return types
const returnTypes = [
{ type: 'FULL', description: 'Return full message on failure' },
{ type: 'HEADERS', description: 'Return headers only' }
];
for (const returnType of returnTypes) {
console.log(`\nTesting DSN return type: ${returnType.description}`);
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: `DSN Return Type: ${returnType.type}`,
text: 'Testing DSN return types',
dsn: {
notify: ['FAILURE'],
returnType: returnType.type as 'FULL' | 'HEADERS'
}
});
// Monitor MAIL FROM command
let mailFromCommand = '';
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
if (command.startsWith('MAIL FROM')) {
mailFromCommand = command;
}
return originalSendCommand(command);
};
await smtpClient.sendMail(email);
// Extract RET parameter
const retMatch = mailFromCommand.match(/RET=([A-Z]+)/);
if (retMatch) {
console.log(` RET parameter: ${retMatch[1]}`);
}
}
await smtpClient.close();
});
tap.test('CEP-10: MDN (Message Disposition Notification)', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Create MDN request email
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Please confirm reading',
text: 'This message requests a read receipt',
headers: {
'Disposition-Notification-To': 'sender@example.com',
'Disposition-Notification-Options': 'signed-receipt-protocol=optional,pkcs7-signature',
'Original-Message-ID': `<${Date.now()}@example.com>`
}
});
const result = await smtpClient.sendMail(email);
expect(result).toBeTruthy();
// Simulate MDN response
const mdnResponse = new Email({
from: 'recipient@example.com',
to: ['sender@example.com'],
subject: 'Read: Please confirm reading',
headers: {
'Content-Type': 'multipart/report; report-type=disposition-notification',
'In-Reply-To': `<${Date.now()}@example.com>`,
'References': `<${Date.now()}@example.com>`,
'Auto-Submitted': 'auto-replied'
},
text: 'The message was displayed to the recipient',
attachments: [{
filename: 'disposition-notification.txt',
content: Buffer.from(`Reporting-UA: mail.example.com; MailClient/1.0
Original-Recipient: rfc822;recipient@example.com
Final-Recipient: rfc822;recipient@example.com
Original-Message-ID: <${Date.now()}@example.com>
Disposition: automatic-action/MDN-sent-automatically; displayed`),
contentType: 'message/disposition-notification'
}]
});
console.log('\nSimulating MDN response...');
await smtpClient.sendMail(mdnResponse);
console.log('MDN response sent successfully');
await smtpClient.close();
});
tap.test('CEP-10: Multiple recipients with different DSN', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Email with multiple recipients, each with different DSN settings
const email = new Email({
from: 'sender@example.com',
to: [
'important@example.com',
'normal@example.com',
'optional@example.com'
],
subject: 'Multi-recipient DSN Test',
text: 'Testing per-recipient DSN options',
dsn: {
recipients: {
'important@example.com': { notify: ['SUCCESS', 'FAILURE', 'DELAY'] },
'normal@example.com': { notify: ['FAILURE'] },
'optional@example.com': { notify: ['NEVER'] }
},
returnType: 'HEADERS'
}
});
// Monitor RCPT TO commands
const rcptCommands: string[] = [];
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
if (command.startsWith('RCPT TO')) {
rcptCommands.push(command);
}
return originalSendCommand(command);
};
await smtpClient.sendMail(email);
console.log('\nPer-recipient DSN settings:');
rcptCommands.forEach(cmd => {
const emailMatch = cmd.match(/<([^>]+)>/);
const notifyMatch = cmd.match(/NOTIFY=([A-Z,]+)/);
if (emailMatch) {
console.log(` ${emailMatch[1]}: ${notifyMatch ? notifyMatch[1] : 'default'}`);
}
});
await smtpClient.close();
});
tap.test('CEP-10: DSN with ORCPT', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Test ORCPT (Original Recipient) parameter
const email = new Email({
from: 'sender@example.com',
to: ['forwarded@example.com'],
subject: 'DSN with ORCPT Test',
text: 'Testing original recipient tracking',
dsn: {
notify: ['SUCCESS', 'FAILURE'],
returnType: 'HEADERS',
orcpt: 'rfc822;original@example.com'
}
});
// Monitor RCPT TO command for ORCPT
let hasOrcpt = false;
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
if (command.includes('ORCPT=')) {
hasOrcpt = true;
console.log('ORCPT parameter found:', command);
}
return originalSendCommand(command);
};
await smtpClient.sendMail(email);
if (!hasOrcpt) {
console.log('ORCPT parameter not included (may not be implemented)');
}
await smtpClient.close();
});
tap.test('CEP-10: Receipt request formats', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Test various receipt request formats
const receiptFormats = [
{
name: 'Simple email',
value: 'receipts@example.com'
},
{
name: 'With display name',
value: '"Receipt Handler" <receipts@example.com>'
},
{
name: 'Multiple addresses',
value: 'receipts@example.com, backup@example.com'
},
{
name: 'With comment',
value: 'receipts@example.com (Automated System)'
}
];
for (const format of receiptFormats) {
console.log(`\nTesting receipt format: ${format.name}`);
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: `Receipt Format: ${format.name}`,
text: 'Testing receipt address formats',
headers: {
'Disposition-Notification-To': format.value
}
});
// Monitor the header
let receiptHeader = '';
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
if (command.toLowerCase().includes('disposition-notification-to:')) {
receiptHeader = command.trim();
}
return originalSendCommand(command);
};
await smtpClient.sendMail(email);
console.log(` Sent as: ${receiptHeader}`);
}
await smtpClient.close();
});
tap.test('CEP-10: Non-delivery reports', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Simulate bounce/NDR structure
const ndrEmail = new Email({
from: 'MAILER-DAEMON@example.com',
to: ['original-sender@example.com'],
subject: 'Undelivered Mail Returned to Sender',
headers: {
'Auto-Submitted': 'auto-replied',
'Content-Type': 'multipart/report; report-type=delivery-status',
'X-Failed-Recipients': 'nonexistent@example.com'
},
text: 'This is the mail delivery agent at example.com.\n\n' +
'I was unable to deliver your message to the following addresses:\n\n' +
'<nonexistent@example.com>: User unknown',
attachments: [
{
filename: 'delivery-status.txt',
content: Buffer.from(`Reporting-MTA: dns; mail.example.com
X-Queue-ID: 123456789
Arrival-Date: ${new Date().toUTCString()}
Final-Recipient: rfc822;nonexistent@example.com
Original-Recipient: rfc822;nonexistent@example.com
Action: failed
Status: 5.1.1
Diagnostic-Code: smtp; 550 5.1.1 User unknown`),
contentType: 'message/delivery-status'
},
{
filename: 'original-message.eml',
content: Buffer.from('From: original-sender@example.com\r\n' +
'To: nonexistent@example.com\r\n' +
'Subject: Original Subject\r\n\r\n' +
'Original message content'),
contentType: 'message/rfc822'
}
]
});
console.log('\nSimulating Non-Delivery Report (NDR)...');
const result = await smtpClient.sendMail(ndrEmail);
expect(result).toBeTruthy();
console.log('NDR sent successfully');
await smtpClient.close();
});
tap.test('CEP-10: Delivery delay notifications', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Simulate delayed delivery notification
const delayNotification = new Email({
from: 'postmaster@example.com',
to: ['sender@example.com'],
subject: 'Delivery Status: Delayed',
headers: {
'Auto-Submitted': 'auto-replied',
'Content-Type': 'multipart/report; report-type=delivery-status',
'X-Delay-Reason': 'Remote server temporarily unavailable'
},
text: 'This is an automatically generated Delivery Delay Notification.\n\n' +
'Your message has not been delivered to the following recipients yet:\n\n' +
' recipient@remote-server.com\n\n' +
'The server will continue trying to deliver your message for 48 hours.',
attachments: [{
filename: 'delay-status.txt',
content: Buffer.from(`Reporting-MTA: dns; mail.example.com
Arrival-Date: ${new Date(Date.now() - 3600000).toUTCString()}
Last-Attempt-Date: ${new Date().toUTCString()}
Final-Recipient: rfc822;recipient@remote-server.com
Action: delayed
Status: 4.4.1
Will-Retry-Until: ${new Date(Date.now() + 172800000).toUTCString()}
Diagnostic-Code: smtp; 421 4.4.1 Remote server temporarily unavailable`),
contentType: 'message/delivery-status'
}]
});
console.log('\nSimulating Delivery Delay Notification...');
await smtpClient.sendMail(delayNotification);
console.log('Delay notification sent successfully');
await smtpClient.close();
});
tap.test('cleanup test SMTP server', async () => {
if (testServer) {
await testServer.stop();
}
});
export default tap.start();