942 lines
28 KiB
TypeScript
942 lines
28 KiB
TypeScript
import * as plugins from '../plugins.js';
|
|
import { EventEmitter } from 'node:events';
|
|
import * as net from 'node:net';
|
|
import * as tls from 'node:tls';
|
|
import { logger } from '../logger.js';
|
|
import {
|
|
SecurityLogger,
|
|
SecurityLogLevel,
|
|
SecurityEventType
|
|
} from '../security/index.js';
|
|
import { UnifiedDeliveryQueue, type IQueueItem } from './classes.delivery.queue.js';
|
|
import type { Email } from '../mta/classes.email.js';
|
|
import type { IDomainRule } from './classes.email.config.js';
|
|
|
|
/**
|
|
* Delivery handler interface
|
|
*/
|
|
export interface IDeliveryHandler {
|
|
deliver(item: IQueueItem): Promise<any>;
|
|
}
|
|
|
|
/**
|
|
* Delivery options
|
|
*/
|
|
export interface IMultiModeDeliveryOptions {
|
|
// Connection options
|
|
connectionPoolSize?: number;
|
|
socketTimeout?: number;
|
|
|
|
// Delivery behavior
|
|
concurrentDeliveries?: number;
|
|
sendTimeout?: number;
|
|
|
|
// TLS options
|
|
verifyCertificates?: boolean;
|
|
tlsMinVersion?: string;
|
|
|
|
// Mode-specific handlers
|
|
forwardHandler?: IDeliveryHandler;
|
|
mtaHandler?: IDeliveryHandler;
|
|
processHandler?: IDeliveryHandler;
|
|
|
|
// Rate limiting
|
|
globalRateLimit?: number;
|
|
perPatternRateLimit?: Record<string, number>;
|
|
|
|
// Event hooks
|
|
onDeliveryStart?: (item: IQueueItem) => Promise<void>;
|
|
onDeliverySuccess?: (item: IQueueItem, result: any) => Promise<void>;
|
|
onDeliveryFailed?: (item: IQueueItem, error: string) => Promise<void>;
|
|
}
|
|
|
|
/**
|
|
* Delivery system statistics
|
|
*/
|
|
export interface IDeliveryStats {
|
|
activeDeliveries: number;
|
|
totalSuccessful: number;
|
|
totalFailed: number;
|
|
avgDeliveryTime: number;
|
|
byMode: {
|
|
forward: {
|
|
successful: number;
|
|
failed: number;
|
|
};
|
|
mta: {
|
|
successful: number;
|
|
failed: number;
|
|
};
|
|
process: {
|
|
successful: number;
|
|
failed: number;
|
|
};
|
|
};
|
|
rateLimiting: {
|
|
currentRate: number;
|
|
globalLimit: number;
|
|
throttled: number;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Handles delivery for all email processing modes
|
|
*/
|
|
export class MultiModeDeliverySystem extends EventEmitter {
|
|
private queue: UnifiedDeliveryQueue;
|
|
private options: Required<IMultiModeDeliveryOptions>;
|
|
private stats: IDeliveryStats;
|
|
private deliveryTimes: number[] = [];
|
|
private activeDeliveries: Set<string> = new Set();
|
|
private running: boolean = false;
|
|
private throttled: boolean = false;
|
|
private rateLimitLastCheck: number = Date.now();
|
|
private rateLimitCounter: number = 0;
|
|
|
|
/**
|
|
* Create a new multi-mode delivery system
|
|
* @param queue Unified delivery queue
|
|
* @param options Delivery options
|
|
*/
|
|
constructor(queue: UnifiedDeliveryQueue, options: IMultiModeDeliveryOptions) {
|
|
super();
|
|
|
|
this.queue = queue;
|
|
|
|
// Set default options
|
|
this.options = {
|
|
connectionPoolSize: options.connectionPoolSize || 10,
|
|
socketTimeout: options.socketTimeout || 30000, // 30 seconds
|
|
concurrentDeliveries: options.concurrentDeliveries || 10,
|
|
sendTimeout: options.sendTimeout || 60000, // 1 minute
|
|
verifyCertificates: options.verifyCertificates !== false, // Default to true
|
|
tlsMinVersion: options.tlsMinVersion || 'TLSv1.2',
|
|
forwardHandler: options.forwardHandler || {
|
|
deliver: this.handleForwardDelivery.bind(this)
|
|
},
|
|
mtaHandler: options.mtaHandler || {
|
|
deliver: this.handleMtaDelivery.bind(this)
|
|
},
|
|
processHandler: options.processHandler || {
|
|
deliver: this.handleProcessDelivery.bind(this)
|
|
},
|
|
globalRateLimit: options.globalRateLimit || 100, // 100 emails per minute
|
|
perPatternRateLimit: options.perPatternRateLimit || {},
|
|
onDeliveryStart: options.onDeliveryStart || (async () => {}),
|
|
onDeliverySuccess: options.onDeliverySuccess || (async () => {}),
|
|
onDeliveryFailed: options.onDeliveryFailed || (async () => {})
|
|
};
|
|
|
|
// Initialize statistics
|
|
this.stats = {
|
|
activeDeliveries: 0,
|
|
totalSuccessful: 0,
|
|
totalFailed: 0,
|
|
avgDeliveryTime: 0,
|
|
byMode: {
|
|
forward: {
|
|
successful: 0,
|
|
failed: 0
|
|
},
|
|
mta: {
|
|
successful: 0,
|
|
failed: 0
|
|
},
|
|
process: {
|
|
successful: 0,
|
|
failed: 0
|
|
}
|
|
},
|
|
rateLimiting: {
|
|
currentRate: 0,
|
|
globalLimit: this.options.globalRateLimit,
|
|
throttled: 0
|
|
}
|
|
};
|
|
|
|
// Set up event listeners
|
|
this.queue.on('itemsReady', this.processItems.bind(this));
|
|
}
|
|
|
|
/**
|
|
* Start the delivery system
|
|
*/
|
|
public async start(): Promise<void> {
|
|
logger.log('info', 'Starting MultiModeDeliverySystem');
|
|
|
|
if (this.running) {
|
|
logger.log('warn', 'MultiModeDeliverySystem is already running');
|
|
return;
|
|
}
|
|
|
|
this.running = true;
|
|
|
|
// Emit started event
|
|
this.emit('started');
|
|
logger.log('info', 'MultiModeDeliverySystem started successfully');
|
|
}
|
|
|
|
/**
|
|
* Stop the delivery system
|
|
*/
|
|
public async stop(): Promise<void> {
|
|
logger.log('info', 'Stopping MultiModeDeliverySystem');
|
|
|
|
if (!this.running) {
|
|
logger.log('warn', 'MultiModeDeliverySystem is already stopped');
|
|
return;
|
|
}
|
|
|
|
this.running = false;
|
|
|
|
// Wait for active deliveries to complete
|
|
if (this.activeDeliveries.size > 0) {
|
|
logger.log('info', `Waiting for ${this.activeDeliveries.size} active deliveries to complete`);
|
|
|
|
// Wait for a maximum of 30 seconds
|
|
await new Promise<void>(resolve => {
|
|
const checkInterval = setInterval(() => {
|
|
if (this.activeDeliveries.size === 0) {
|
|
clearInterval(checkInterval);
|
|
resolve();
|
|
}
|
|
}, 1000);
|
|
|
|
// Force resolve after 30 seconds
|
|
setTimeout(() => {
|
|
clearInterval(checkInterval);
|
|
resolve();
|
|
}, 30000);
|
|
});
|
|
}
|
|
|
|
// Emit stopped event
|
|
this.emit('stopped');
|
|
logger.log('info', 'MultiModeDeliverySystem stopped successfully');
|
|
}
|
|
|
|
/**
|
|
* Process ready items from the queue
|
|
* @param items Queue items ready for processing
|
|
*/
|
|
private async processItems(items: IQueueItem[]): Promise<void> {
|
|
if (!this.running) {
|
|
return;
|
|
}
|
|
|
|
// Check if we're already at max concurrent deliveries
|
|
if (this.activeDeliveries.size >= this.options.concurrentDeliveries) {
|
|
logger.log('debug', `Already at max concurrent deliveries (${this.activeDeliveries.size})`);
|
|
return;
|
|
}
|
|
|
|
// Check rate limiting
|
|
if (this.checkRateLimit()) {
|
|
logger.log('debug', 'Rate limit exceeded, throttling deliveries');
|
|
return;
|
|
}
|
|
|
|
// Calculate how many more deliveries we can start
|
|
const availableSlots = this.options.concurrentDeliveries - this.activeDeliveries.size;
|
|
const itemsToProcess = items.slice(0, availableSlots);
|
|
|
|
if (itemsToProcess.length === 0) {
|
|
return;
|
|
}
|
|
|
|
logger.log('info', `Processing ${itemsToProcess.length} items for delivery`);
|
|
|
|
// Process each item
|
|
for (const item of itemsToProcess) {
|
|
// Mark as processing
|
|
await this.queue.markProcessing(item.id);
|
|
|
|
// Add to active deliveries
|
|
this.activeDeliveries.add(item.id);
|
|
this.stats.activeDeliveries = this.activeDeliveries.size;
|
|
|
|
// Deliver asynchronously
|
|
this.deliverItem(item).catch(err => {
|
|
logger.log('error', `Unhandled error in delivery: ${err.message}`);
|
|
});
|
|
}
|
|
|
|
// Update statistics
|
|
this.emit('statsUpdated', this.stats);
|
|
}
|
|
|
|
/**
|
|
* Deliver an item from the queue
|
|
* @param item Queue item to deliver
|
|
*/
|
|
private async deliverItem(item: IQueueItem): Promise<void> {
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
// Call delivery start hook
|
|
await this.options.onDeliveryStart(item);
|
|
|
|
// Emit delivery start event
|
|
this.emit('deliveryStart', item);
|
|
logger.log('info', `Starting delivery of item ${item.id}, mode: ${item.processingMode}`);
|
|
|
|
// Choose the appropriate handler based on mode
|
|
let result: any;
|
|
|
|
switch (item.processingMode) {
|
|
case 'forward':
|
|
result = await this.options.forwardHandler.deliver(item);
|
|
break;
|
|
|
|
case 'mta':
|
|
result = await this.options.mtaHandler.deliver(item);
|
|
break;
|
|
|
|
case 'process':
|
|
result = await this.options.processHandler.deliver(item);
|
|
break;
|
|
|
|
default:
|
|
throw new Error(`Unknown processing mode: ${item.processingMode}`);
|
|
}
|
|
|
|
// Mark as delivered
|
|
await this.queue.markDelivered(item.id);
|
|
|
|
// Update statistics
|
|
this.stats.totalSuccessful++;
|
|
this.stats.byMode[item.processingMode].successful++;
|
|
|
|
// Calculate delivery time
|
|
const deliveryTime = Date.now() - startTime;
|
|
this.deliveryTimes.push(deliveryTime);
|
|
this.updateDeliveryTimeStats();
|
|
|
|
// Call delivery success hook
|
|
await this.options.onDeliverySuccess(item, result);
|
|
|
|
// Emit delivery success event
|
|
this.emit('deliverySuccess', item, result);
|
|
logger.log('info', `Item ${item.id} delivered successfully in ${deliveryTime}ms`);
|
|
|
|
SecurityLogger.getInstance().logEvent({
|
|
level: SecurityLogLevel.INFO,
|
|
type: SecurityEventType.EMAIL_DELIVERY,
|
|
message: 'Email delivery successful',
|
|
details: {
|
|
itemId: item.id,
|
|
mode: item.processingMode,
|
|
pattern: item.rule.pattern,
|
|
deliveryTime
|
|
},
|
|
success: true
|
|
});
|
|
} catch (error) {
|
|
// Calculate delivery attempt time even for failures
|
|
const deliveryTime = Date.now() - startTime;
|
|
|
|
// Mark as failed
|
|
await this.queue.markFailed(item.id, error.message);
|
|
|
|
// Update statistics
|
|
this.stats.totalFailed++;
|
|
this.stats.byMode[item.processingMode].failed++;
|
|
|
|
// Call delivery failed hook
|
|
await this.options.onDeliveryFailed(item, error.message);
|
|
|
|
// Emit delivery failed event
|
|
this.emit('deliveryFailed', item, error);
|
|
logger.log('error', `Item ${item.id} delivery failed: ${error.message}`);
|
|
|
|
SecurityLogger.getInstance().logEvent({
|
|
level: SecurityLogLevel.ERROR,
|
|
type: SecurityEventType.EMAIL_DELIVERY,
|
|
message: 'Email delivery failed',
|
|
details: {
|
|
itemId: item.id,
|
|
mode: item.processingMode,
|
|
pattern: item.rule.pattern,
|
|
error: error.message,
|
|
deliveryTime
|
|
},
|
|
success: false
|
|
});
|
|
} finally {
|
|
// Remove from active deliveries
|
|
this.activeDeliveries.delete(item.id);
|
|
this.stats.activeDeliveries = this.activeDeliveries.size;
|
|
|
|
// Update statistics
|
|
this.emit('statsUpdated', this.stats);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Default handler for forward mode delivery
|
|
* @param item Queue item
|
|
*/
|
|
private async handleForwardDelivery(item: IQueueItem): Promise<any> {
|
|
logger.log('info', `Forward delivery for item ${item.id}`);
|
|
|
|
const email = item.processingResult as Email;
|
|
const rule = item.rule;
|
|
|
|
// Get target server information
|
|
const targetServer = rule.target?.server;
|
|
const targetPort = rule.target?.port || 25;
|
|
const useTls = rule.target?.useTls ?? false;
|
|
|
|
if (!targetServer) {
|
|
throw new Error('No target server configured for forward mode');
|
|
}
|
|
|
|
logger.log('info', `Forwarding email to ${targetServer}:${targetPort}, TLS: ${useTls}`);
|
|
|
|
// Create a socket connection to the target server
|
|
const socket = new net.Socket();
|
|
|
|
// Set timeout
|
|
socket.setTimeout(this.options.socketTimeout);
|
|
|
|
try {
|
|
// Connect to the target server
|
|
await new Promise<void>((resolve, reject) => {
|
|
// Handle connection events
|
|
socket.on('connect', () => {
|
|
logger.log('debug', `Connected to ${targetServer}:${targetPort}`);
|
|
resolve();
|
|
});
|
|
|
|
socket.on('timeout', () => {
|
|
reject(new Error(`Connection timeout to ${targetServer}:${targetPort}`));
|
|
});
|
|
|
|
socket.on('error', (err) => {
|
|
reject(new Error(`Connection error to ${targetServer}:${targetPort}: ${err.message}`));
|
|
});
|
|
|
|
// Connect to the server
|
|
socket.connect({
|
|
host: targetServer,
|
|
port: targetPort
|
|
});
|
|
});
|
|
|
|
// Implement SMTP protocol here
|
|
// This is a simplified implementation
|
|
|
|
// Send EHLO
|
|
await this.smtpCommand(socket, `EHLO ${rule.mtaOptions?.domain || 'localhost'}`);
|
|
|
|
// Start TLS if required
|
|
if (useTls) {
|
|
await this.smtpCommand(socket, 'STARTTLS');
|
|
|
|
// Upgrade to TLS
|
|
const tlsSocket = await this.upgradeTls(socket, targetServer);
|
|
|
|
// Send EHLO again after STARTTLS
|
|
await this.smtpCommand(tlsSocket, `EHLO ${rule.mtaOptions?.domain || 'localhost'}`);
|
|
|
|
// Use tlsSocket for remaining commands
|
|
return this.completeSMTPExchange(tlsSocket, email, rule);
|
|
}
|
|
|
|
// Complete the SMTP exchange
|
|
return this.completeSMTPExchange(socket, email, rule);
|
|
} catch (error) {
|
|
logger.log('error', `Failed to forward email: ${error.message}`);
|
|
|
|
// Close the connection
|
|
socket.destroy();
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Complete the SMTP exchange after connection and initial setup
|
|
* @param socket Network socket
|
|
* @param email Email to send
|
|
* @param rule Domain rule
|
|
*/
|
|
private async completeSMTPExchange(socket: net.Socket | tls.TLSSocket, email: Email, rule: IDomainRule): Promise<any> {
|
|
try {
|
|
// Authenticate if credentials provided
|
|
if (rule.target?.authentication?.user && rule.target?.authentication?.pass) {
|
|
// Send AUTH LOGIN
|
|
await this.smtpCommand(socket, 'AUTH LOGIN');
|
|
|
|
// Send username (base64)
|
|
const username = Buffer.from(rule.target.authentication.user).toString('base64');
|
|
await this.smtpCommand(socket, username);
|
|
|
|
// Send password (base64)
|
|
const password = Buffer.from(rule.target.authentication.pass).toString('base64');
|
|
await this.smtpCommand(socket, password);
|
|
}
|
|
|
|
// Send MAIL FROM
|
|
await this.smtpCommand(socket, `MAIL FROM:<${email.from}>`);
|
|
|
|
// Send RCPT TO for each recipient
|
|
for (const recipient of email.getAllRecipients()) {
|
|
await this.smtpCommand(socket, `RCPT TO:<${recipient}>`);
|
|
}
|
|
|
|
// Send DATA
|
|
await this.smtpCommand(socket, 'DATA');
|
|
|
|
// Send email content (simplified)
|
|
const emailContent = await this.getFormattedEmail(email);
|
|
await this.smtpData(socket, emailContent);
|
|
|
|
// Send QUIT
|
|
await this.smtpCommand(socket, 'QUIT');
|
|
|
|
// Close the connection
|
|
socket.end();
|
|
|
|
logger.log('info', `Email forwarded successfully to ${rule.target?.server}:${rule.target?.port || 25}`);
|
|
|
|
return {
|
|
targetServer: rule.target?.server,
|
|
targetPort: rule.target?.port || 25,
|
|
recipients: email.getAllRecipients().length
|
|
};
|
|
} catch (error: any) {
|
|
logger.log('error', `Failed to forward email: ${error.message}`);
|
|
|
|
// Close the connection
|
|
socket.destroy();
|
|
|
|
throw error;
|
|
}
|
|
socket.destroy();
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Default handler for MTA mode delivery
|
|
* @param item Queue item
|
|
*/
|
|
private async handleMtaDelivery(item: IQueueItem): Promise<any> {
|
|
logger.log('info', `MTA delivery for item ${item.id}`);
|
|
|
|
const email = item.processingResult as Email;
|
|
const rule = item.rule;
|
|
|
|
try {
|
|
// In a full implementation, this would use the MTA service
|
|
// For now, we'll simulate a successful delivery
|
|
|
|
logger.log('info', `Email processed by MTA: ${email.subject} to ${email.getAllRecipients().join(', ')}`);
|
|
|
|
// Apply MTA rule options if provided
|
|
if (rule.mtaOptions) {
|
|
const options = rule.mtaOptions;
|
|
|
|
// Apply DKIM signing if enabled
|
|
if (options.dkimSign && options.dkimOptions) {
|
|
// Sign the email with DKIM
|
|
logger.log('info', `Signing email with DKIM for domain ${options.dkimOptions.domainName}`);
|
|
|
|
// In a full implementation, this would use the DKIM signing library
|
|
}
|
|
}
|
|
|
|
// Simulate successful delivery
|
|
return {
|
|
recipients: email.getAllRecipients().length,
|
|
subject: email.subject,
|
|
dkimSigned: !!rule.mtaOptions?.dkimSign
|
|
};
|
|
} catch (error) {
|
|
logger.log('error', `Failed to process email in MTA mode: ${error.message}`);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Default handler for process mode delivery
|
|
* @param item Queue item
|
|
*/
|
|
private async handleProcessDelivery(item: IQueueItem): Promise<any> {
|
|
logger.log('info', `Process delivery for item ${item.id}`);
|
|
|
|
const email = item.processingResult as Email;
|
|
const rule = item.rule;
|
|
|
|
try {
|
|
// Apply content scanning if enabled
|
|
if (rule.contentScanning && rule.scanners && rule.scanners.length > 0) {
|
|
logger.log('info', 'Performing content scanning');
|
|
|
|
// Apply each scanner
|
|
for (const scanner of rule.scanners) {
|
|
switch (scanner.type) {
|
|
case 'spam':
|
|
logger.log('info', 'Scanning for spam content');
|
|
// Implement spam scanning
|
|
break;
|
|
|
|
case 'virus':
|
|
logger.log('info', 'Scanning for virus content');
|
|
// Implement virus scanning
|
|
break;
|
|
|
|
case 'attachment':
|
|
logger.log('info', 'Scanning attachments');
|
|
|
|
// Check for blocked extensions
|
|
if (scanner.blockedExtensions && scanner.blockedExtensions.length > 0) {
|
|
for (const attachment of email.attachments) {
|
|
const ext = this.getFileExtension(attachment.filename);
|
|
if (scanner.blockedExtensions.includes(ext)) {
|
|
if (scanner.action === 'reject') {
|
|
throw new Error(`Blocked attachment type: ${ext}`);
|
|
} else { // tag
|
|
email.addHeader('X-Attachment-Warning', `Potentially unsafe attachment: ${attachment.filename}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply transformations if defined
|
|
if (rule.transformations && rule.transformations.length > 0) {
|
|
logger.log('info', 'Applying email transformations');
|
|
|
|
for (const transform of rule.transformations) {
|
|
switch (transform.type) {
|
|
case 'addHeader':
|
|
if (transform.header && transform.value) {
|
|
email.addHeader(transform.header, transform.value);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
logger.log('info', `Email successfully processed in store-and-forward mode`);
|
|
|
|
// Simulate successful delivery
|
|
return {
|
|
recipients: email.getAllRecipients().length,
|
|
subject: email.subject,
|
|
scanned: !!rule.contentScanning,
|
|
transformed: !!(rule.transformations && rule.transformations.length > 0)
|
|
};
|
|
} catch (error) {
|
|
logger.log('error', `Failed to process email: ${error.message}`);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get file extension from filename
|
|
*/
|
|
private getFileExtension(filename: string): string {
|
|
return filename.substring(filename.lastIndexOf('.')).toLowerCase();
|
|
}
|
|
|
|
/**
|
|
* Format email for SMTP transmission
|
|
* @param email Email to format
|
|
*/
|
|
private async getFormattedEmail(email: Email): Promise<string> {
|
|
// This is a simplified implementation
|
|
// In a full implementation, this would use proper MIME formatting
|
|
|
|
let content = '';
|
|
|
|
// Add headers
|
|
content += `From: ${email.from}\r\n`;
|
|
content += `To: ${email.to}\r\n`;
|
|
content += `Subject: ${email.subject}\r\n`;
|
|
|
|
// Add additional headers
|
|
for (const [name, value] of Object.entries(email.headers || {})) {
|
|
content += `${name}: ${value}\r\n`;
|
|
}
|
|
|
|
// Add content type for multipart
|
|
if (email.attachments && email.attachments.length > 0) {
|
|
const boundary = `----_=_NextPart_${Math.random().toString(36).substr(2)}`;
|
|
content += `MIME-Version: 1.0\r\n`;
|
|
content += `Content-Type: multipart/mixed; boundary="${boundary}"\r\n`;
|
|
content += `\r\n`;
|
|
|
|
// Add text part
|
|
content += `--${boundary}\r\n`;
|
|
content += `Content-Type: text/plain; charset="UTF-8"\r\n`;
|
|
content += `\r\n`;
|
|
content += `${email.text}\r\n`;
|
|
|
|
// Add HTML part if present
|
|
if (email.html) {
|
|
content += `--${boundary}\r\n`;
|
|
content += `Content-Type: text/html; charset="UTF-8"\r\n`;
|
|
content += `\r\n`;
|
|
content += `${email.html}\r\n`;
|
|
}
|
|
|
|
// Add attachments
|
|
for (const attachment of email.attachments) {
|
|
content += `--${boundary}\r\n`;
|
|
content += `Content-Type: ${attachment.contentType || 'application/octet-stream'}; name="${attachment.filename}"\r\n`;
|
|
content += `Content-Disposition: attachment; filename="${attachment.filename}"\r\n`;
|
|
content += `Content-Transfer-Encoding: base64\r\n`;
|
|
content += `\r\n`;
|
|
|
|
// Add base64 encoded content
|
|
const base64Content = attachment.content.toString('base64');
|
|
|
|
// Split into lines of 76 characters
|
|
for (let i = 0; i < base64Content.length; i += 76) {
|
|
content += base64Content.substring(i, i + 76) + '\r\n';
|
|
}
|
|
}
|
|
|
|
// End boundary
|
|
content += `--${boundary}--\r\n`;
|
|
} else {
|
|
// Simple email with just text
|
|
content += `Content-Type: text/plain; charset="UTF-8"\r\n`;
|
|
content += `\r\n`;
|
|
content += `${email.text}\r\n`;
|
|
}
|
|
|
|
return content;
|
|
}
|
|
|
|
/**
|
|
* Send SMTP command and wait for response
|
|
* @param socket Socket connection
|
|
* @param command SMTP command to send
|
|
*/
|
|
private async smtpCommand(socket: net.Socket, command: string): Promise<string> {
|
|
return new Promise<string>((resolve, reject) => {
|
|
const onData = (data: Buffer) => {
|
|
const response = data.toString().trim();
|
|
|
|
// Clean up listeners
|
|
socket.removeListener('data', onData);
|
|
socket.removeListener('error', onError);
|
|
socket.removeListener('timeout', onTimeout);
|
|
|
|
// Check response code
|
|
if (response.charAt(0) === '2' || response.charAt(0) === '3') {
|
|
resolve(response);
|
|
} else {
|
|
reject(new Error(`SMTP error: ${response}`));
|
|
}
|
|
};
|
|
|
|
const onError = (err: Error) => {
|
|
// Clean up listeners
|
|
socket.removeListener('data', onData);
|
|
socket.removeListener('error', onError);
|
|
socket.removeListener('timeout', onTimeout);
|
|
|
|
reject(err);
|
|
};
|
|
|
|
const onTimeout = () => {
|
|
// Clean up listeners
|
|
socket.removeListener('data', onData);
|
|
socket.removeListener('error', onError);
|
|
socket.removeListener('timeout', onTimeout);
|
|
|
|
reject(new Error('SMTP command timeout'));
|
|
};
|
|
|
|
// Set up listeners
|
|
socket.once('data', onData);
|
|
socket.once('error', onError);
|
|
socket.once('timeout', onTimeout);
|
|
|
|
// Send command
|
|
socket.write(command + '\r\n');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Send SMTP DATA command with content
|
|
* @param socket Socket connection
|
|
* @param data Email content to send
|
|
*/
|
|
private async smtpData(socket: net.Socket, data: string): Promise<string> {
|
|
return new Promise<string>((resolve, reject) => {
|
|
const onData = (responseData: Buffer) => {
|
|
const response = responseData.toString().trim();
|
|
|
|
// Clean up listeners
|
|
socket.removeListener('data', onData);
|
|
socket.removeListener('error', onError);
|
|
socket.removeListener('timeout', onTimeout);
|
|
|
|
// Check response code
|
|
if (response.charAt(0) === '2') {
|
|
resolve(response);
|
|
} else {
|
|
reject(new Error(`SMTP error: ${response}`));
|
|
}
|
|
};
|
|
|
|
const onError = (err: Error) => {
|
|
// Clean up listeners
|
|
socket.removeListener('data', onData);
|
|
socket.removeListener('error', onError);
|
|
socket.removeListener('timeout', onTimeout);
|
|
|
|
reject(err);
|
|
};
|
|
|
|
const onTimeout = () => {
|
|
// Clean up listeners
|
|
socket.removeListener('data', onData);
|
|
socket.removeListener('error', onError);
|
|
socket.removeListener('timeout', onTimeout);
|
|
|
|
reject(new Error('SMTP data timeout'));
|
|
};
|
|
|
|
// Set up listeners
|
|
socket.once('data', onData);
|
|
socket.once('error', onError);
|
|
socket.once('timeout', onTimeout);
|
|
|
|
// Send data and end with CRLF.CRLF
|
|
socket.write(data + '\r\n.\r\n');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Upgrade socket to TLS
|
|
* @param socket Socket connection
|
|
* @param hostname Target hostname for TLS
|
|
*/
|
|
private async upgradeTls(socket: net.Socket, hostname: string): Promise<tls.TLSSocket> {
|
|
return new Promise<tls.TLSSocket>((resolve, reject) => {
|
|
const tlsOptions: tls.ConnectionOptions = {
|
|
socket,
|
|
servername: hostname,
|
|
rejectUnauthorized: this.options.verifyCertificates,
|
|
minVersion: this.options.tlsMinVersion as tls.SecureVersion
|
|
};
|
|
|
|
const tlsSocket = tls.connect(tlsOptions);
|
|
|
|
tlsSocket.once('secureConnect', () => {
|
|
resolve(tlsSocket);
|
|
});
|
|
|
|
tlsSocket.once('error', (err) => {
|
|
reject(new Error(`TLS error: ${err.message}`));
|
|
});
|
|
|
|
tlsSocket.setTimeout(this.options.socketTimeout);
|
|
|
|
tlsSocket.once('timeout', () => {
|
|
reject(new Error('TLS connection timeout'));
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update delivery time statistics
|
|
*/
|
|
private updateDeliveryTimeStats(): void {
|
|
if (this.deliveryTimes.length === 0) return;
|
|
|
|
// Keep only the last 1000 delivery times
|
|
if (this.deliveryTimes.length > 1000) {
|
|
this.deliveryTimes = this.deliveryTimes.slice(-1000);
|
|
}
|
|
|
|
// Calculate average
|
|
const sum = this.deliveryTimes.reduce((acc, time) => acc + time, 0);
|
|
this.stats.avgDeliveryTime = sum / this.deliveryTimes.length;
|
|
}
|
|
|
|
/**
|
|
* Check if rate limit is exceeded
|
|
* @returns True if rate limited, false otherwise
|
|
*/
|
|
private checkRateLimit(): boolean {
|
|
const now = Date.now();
|
|
const elapsed = now - this.rateLimitLastCheck;
|
|
|
|
// Reset counter if more than a minute has passed
|
|
if (elapsed >= 60000) {
|
|
this.rateLimitLastCheck = now;
|
|
this.rateLimitCounter = 0;
|
|
this.throttled = false;
|
|
this.stats.rateLimiting.currentRate = 0;
|
|
return false;
|
|
}
|
|
|
|
// Check if we're already throttled
|
|
if (this.throttled) {
|
|
return true;
|
|
}
|
|
|
|
// Increment counter
|
|
this.rateLimitCounter++;
|
|
|
|
// Calculate current rate (emails per minute)
|
|
const rate = (this.rateLimitCounter / elapsed) * 60000;
|
|
this.stats.rateLimiting.currentRate = rate;
|
|
|
|
// Check if rate limit is exceeded
|
|
if (rate > this.options.globalRateLimit) {
|
|
this.throttled = true;
|
|
this.stats.rateLimiting.throttled++;
|
|
|
|
// Schedule throttle reset
|
|
const resetDelay = 60000 - elapsed;
|
|
setTimeout(() => {
|
|
this.throttled = false;
|
|
this.rateLimitLastCheck = Date.now();
|
|
this.rateLimitCounter = 0;
|
|
this.stats.rateLimiting.currentRate = 0;
|
|
}, resetDelay);
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Update delivery options
|
|
* @param options New options
|
|
*/
|
|
public updateOptions(options: Partial<IMultiModeDeliveryOptions>): void {
|
|
this.options = {
|
|
...this.options,
|
|
...options
|
|
};
|
|
|
|
// Update rate limit statistics
|
|
if (options.globalRateLimit) {
|
|
this.stats.rateLimiting.globalLimit = options.globalRateLimit;
|
|
}
|
|
|
|
logger.log('info', 'MultiModeDeliverySystem options updated');
|
|
}
|
|
|
|
/**
|
|
* Get delivery statistics
|
|
*/
|
|
public getStats(): IDeliveryStats {
|
|
return { ...this.stats };
|
|
}
|
|
} |