dcrouter/ts/mail/routing/classes.unified.email.server.ts
Philipp Kunz b11fea7334 feat(socket-handler): implement direct socket passing for DNS and email services
- Add socket-handler mode eliminating internal port binding for improved performance
- Add `dnsDomain` config option for automatic DNS-over-HTTPS (DoH) setup
- Add `useSocketHandler` flag to email config for direct socket processing
- Update SmartProxy route generation to support socket-handler actions
- Integrate smartdns with manual HTTPS mode for DoH without port binding
- Add automatic route creation for DNS paths when dnsDomain is configured
- Update documentation with socket-handler configuration and benefits
- Improve resource efficiency by eliminating internal port forwarding
2025-05-29 16:26:19 +00:00

1722 lines
55 KiB
TypeScript

import * as plugins from '../../plugins.js';
import * as paths from '../../paths.js';
import { EventEmitter } from 'events';
import { logger } from '../../logger.js';
import {
SecurityLogger,
SecurityLogLevel,
SecurityEventType
} from '../../security/index.js';
import { DKIMCreator } from '../security/classes.dkimcreator.js';
import { IPReputationChecker } from '../../security/classes.ipreputationchecker.js';
import {
IPWarmupManager,
type IIPWarmupConfig,
SenderReputationMonitor,
type IReputationMonitorConfig
} from '../../deliverability/index.js';
import { EmailRouter } from './classes.email.router.js';
import type { IEmailRoute, IEmailAction, IEmailContext } from './interfaces.js';
import { Email } from '../core/classes.email.js';
import { BounceManager, BounceType, BounceCategory } from '../core/classes.bouncemanager.js';
import { createSmtpServer } from '../delivery/smtpserver/index.js';
import { createPooledSmtpClient } from '../delivery/smtpclient/create-client.js';
import type { SmtpClient } from '../delivery/smtpclient/smtp-client.js';
import { MultiModeDeliverySystem, type IMultiModeDeliveryOptions } from '../delivery/classes.delivery.system.js';
import { UnifiedDeliveryQueue, type IQueueOptions } from '../delivery/classes.delivery.queue.js';
import { UnifiedRateLimiter, type IHierarchicalRateLimits } from '../delivery/classes.unified.rate.limiter.js';
import { SmtpState } from '../delivery/interfaces.js';
import type { EmailProcessingMode, ISmtpSession as IBaseSmtpSession } from '../delivery/interfaces.js';
import type { DcRouter } from '../../classes.dcrouter.js';
/**
* Extended SMTP session interface with route information
*/
export interface IExtendedSmtpSession extends ISmtpSession {
/**
* Matched route for this session
*/
matchedRoute?: IEmailRoute;
}
/**
* Options for the unified email server
*/
export interface IUnifiedEmailServerOptions {
// Base server options
ports: number[];
hostname: string;
domains: string[]; // Domains to handle email for
banner?: string;
debug?: boolean;
useSocketHandler?: boolean; // Use socket-handler mode instead of port listening
// Authentication options
auth?: {
required?: boolean;
methods?: ('PLAIN' | 'LOGIN' | 'OAUTH2')[];
users?: Array<{username: string, password: string}>;
};
// TLS options
tls?: {
certPath?: string;
keyPath?: string;
caPath?: string;
minVersion?: string;
ciphers?: string;
};
// Limits
maxMessageSize?: number;
maxClients?: number;
maxConnections?: number;
// Connection options
connectionTimeout?: number;
socketTimeout?: number;
// Email routing rules
routes: IEmailRoute[];
// Outbound settings
outbound?: {
maxConnections?: number;
connectionTimeout?: number;
socketTimeout?: number;
retryAttempts?: number;
defaultFrom?: string;
};
// DKIM settings
dkim?: {
enabled: boolean;
selector?: string;
keySize?: number;
};
// Rate limiting
rateLimits?: IHierarchicalRateLimits;
// Deliverability options
ipWarmupConfig?: IIPWarmupConfig;
reputationMonitorConfig?: IReputationMonitorConfig;
}
/**
* Extended SMTP session interface for UnifiedEmailServer
*/
export interface ISmtpSession extends IBaseSmtpSession {
/**
* User information if authenticated
*/
user?: {
username: string;
[key: string]: any;
};
/**
* Matched route for this session
*/
matchedRoute?: IEmailRoute;
}
/**
* Authentication data for SMTP
*/
import type { ISmtpAuth } from '../delivery/interfaces.js';
export type IAuthData = ISmtpAuth;
/**
* Server statistics
*/
export interface IServerStats {
startTime: Date;
connections: {
current: number;
total: number;
};
messages: {
processed: number;
delivered: number;
failed: number;
};
processingTime: {
avg: number;
max: number;
min: number;
};
}
/**
* Unified email server that handles all email traffic with pattern-based routing
*/
export class UnifiedEmailServer extends EventEmitter {
private dcRouter: DcRouter;
private options: IUnifiedEmailServerOptions;
private emailRouter: EmailRouter;
private servers: any[] = [];
private stats: IServerStats;
// Add components needed for sending and securing emails
public dkimCreator: DKIMCreator;
private ipReputationChecker: IPReputationChecker; // TODO: Implement IP reputation checks in processEmailByMode
private bounceManager: BounceManager;
private ipWarmupManager: IPWarmupManager;
private senderReputationMonitor: SenderReputationMonitor;
public deliveryQueue: UnifiedDeliveryQueue;
public deliverySystem: MultiModeDeliverySystem;
private rateLimiter: UnifiedRateLimiter; // TODO: Implement rate limiting in SMTP server handlers
private dkimKeys: Map<string, string> = new Map(); // domain -> private key
private smtpClients: Map<string, SmtpClient> = new Map(); // host:port -> client
constructor(dcRouter: DcRouter, options: IUnifiedEmailServerOptions) {
super();
this.dcRouter = dcRouter;
// Set default options
this.options = {
...options,
banner: options.banner || `${options.hostname} ESMTP UnifiedEmailServer`,
maxMessageSize: options.maxMessageSize || 10 * 1024 * 1024, // 10MB
maxClients: options.maxClients || 100,
maxConnections: options.maxConnections || 1000,
connectionTimeout: options.connectionTimeout || 60000, // 1 minute
socketTimeout: options.socketTimeout || 60000 // 1 minute
};
// Initialize DKIM creator
this.dkimCreator = new DKIMCreator(paths.keysDir);
// Initialize IP reputation checker
this.ipReputationChecker = IPReputationChecker.getInstance({
enableLocalCache: true,
enableDNSBL: true,
enableIPInfo: true
});
// Initialize bounce manager
this.bounceManager = new BounceManager({
maxCacheSize: 10000,
cacheTTL: 30 * 24 * 60 * 60 * 1000 // 30 days
});
// Initialize IP warmup manager
this.ipWarmupManager = IPWarmupManager.getInstance(options.ipWarmupConfig || {
enabled: true,
ipAddresses: [],
targetDomains: []
});
// Initialize sender reputation monitor
this.senderReputationMonitor = SenderReputationMonitor.getInstance(options.reputationMonitorConfig || {
enabled: true,
domains: []
});
// Initialize email router with routes
this.emailRouter = new EmailRouter(options.routes || []);
// Initialize rate limiter
this.rateLimiter = new UnifiedRateLimiter(options.rateLimits || {
global: {
maxConnectionsPerIP: 10,
maxMessagesPerMinute: 100,
maxRecipientsPerMessage: 50,
maxErrorsPerIP: 10,
maxAuthFailuresPerIP: 5,
blockDuration: 300000 // 5 minutes
}
});
// Initialize delivery components
const queueOptions: IQueueOptions = {
storageType: 'memory', // Default to memory storage
maxRetries: 3,
baseRetryDelay: 300000, // 5 minutes
maxRetryDelay: 3600000 // 1 hour
};
this.deliveryQueue = new UnifiedDeliveryQueue(queueOptions);
const deliveryOptions: IMultiModeDeliveryOptions = {
globalRateLimit: 100, // Default to 100 emails per minute
concurrentDeliveries: 10,
processBounces: true,
bounceHandler: {
processSmtpFailure: this.processSmtpFailure.bind(this)
},
onDeliverySuccess: async (item, _result) => {
// Record delivery success event for reputation monitoring
const email = item.processingResult as Email;
const senderDomain = email.from.split('@')[1];
if (senderDomain) {
this.recordReputationEvent(senderDomain, {
type: 'delivered',
count: email.to.length
});
}
}
};
this.deliverySystem = new MultiModeDeliverySystem(this.deliveryQueue, deliveryOptions, this);
// Initialize statistics
this.stats = {
startTime: new Date(),
connections: {
current: 0,
total: 0
},
messages: {
processed: 0,
delivered: 0,
failed: 0
},
processingTime: {
avg: 0,
max: 0,
min: 0
}
};
// We'll create the SMTP servers during the start() method
}
/**
* Get or create an SMTP client for the given host and port
* Uses connection pooling for efficiency
*/
public getSmtpClient(host: string, port: number = 25): SmtpClient {
const clientKey = `${host}:${port}`;
// Check if we already have a client for this destination
let client = this.smtpClients.get(clientKey);
if (!client) {
// Create a new pooled SMTP client
client = createPooledSmtpClient({
host,
port,
secure: port === 465,
connectionTimeout: this.options.outbound?.connectionTimeout || 30000,
socketTimeout: this.options.outbound?.socketTimeout || 120000,
maxConnections: this.options.outbound?.maxConnections || 10,
maxMessages: 1000, // Messages per connection before reconnect
pool: true,
debug: false
});
this.smtpClients.set(clientKey, client);
logger.log('info', `Created new SMTP client pool for ${clientKey}`);
}
return client;
}
/**
* Start the unified email server
*/
public async start(): Promise<void> {
logger.log('info', `Starting UnifiedEmailServer on ports: ${(this.options.ports as number[]).join(', ')}`);
try {
// Initialize the delivery queue
await this.deliveryQueue.initialize();
logger.log('info', 'Email delivery queue initialized');
// Start the delivery system
await this.deliverySystem.start();
logger.log('info', 'Email delivery system started');
// Set up automatic DKIM if DNS server is available
if (this.dcRouter.dnsServer && this.options.dkim?.enabled) {
await this.setupAutomaticDkim();
logger.log('info', 'Automatic DKIM configuration completed');
}
// Skip server creation in socket-handler mode
if (this.options.useSocketHandler) {
logger.log('info', 'UnifiedEmailServer started in socket-handler mode (no port listening)');
this.emit('started');
return;
}
// Ensure we have the necessary TLS options
const hasTlsConfig = this.options.tls?.keyPath && this.options.tls?.certPath;
// Prepare the certificate and key if available
let key: string | undefined;
let cert: string | undefined;
if (hasTlsConfig) {
try {
key = plugins.fs.readFileSync(this.options.tls.keyPath!, 'utf8');
cert = plugins.fs.readFileSync(this.options.tls.certPath!, 'utf8');
logger.log('info', 'TLS certificates loaded successfully');
} catch (error) {
logger.log('warn', `Failed to load TLS certificates: ${error.message}`);
}
}
// Create a SMTP server for each port
for (const port of this.options.ports as number[]) {
// Create a reference object to hold the MTA service during setup
const mtaRef = {
config: {
smtp: {
hostname: this.options.hostname
},
security: {
checkIPReputation: false,
verifyDkim: true,
verifySpf: true,
verifyDmarc: true
}
},
// These will be implemented in the real integration:
dkimVerifier: {
verify: async () => ({ isValid: true, domain: '' })
},
spfVerifier: {
verifyAndApply: async () => true
},
dmarcVerifier: {
verify: async () => ({}),
applyPolicy: () => true
},
processIncomingEmail: async (email: Email) => {
// Process email using the new route-based system
await this.processEmailByMode(email, {
id: 'session-' + Math.random().toString(36).substring(2),
state: SmtpState.FINISHED,
mailFrom: email.from,
rcptTo: email.to,
emailData: email.toRFC822String(), // Use the proper method to get the full email content
useTLS: false,
connectionEnded: true,
remoteAddress: '127.0.0.1',
clientHostname: '',
secure: false,
authenticated: false,
envelope: {
mailFrom: { address: email.from, args: {} },
rcptTo: email.to.map(recipient => ({ address: recipient, args: {} }))
}
});
return true;
}
};
// Create server options
const serverOptions = {
port,
hostname: this.options.hostname,
key,
cert
};
// Create and start the SMTP server
const smtpServer = createSmtpServer(mtaRef as any, serverOptions);
this.servers.push(smtpServer);
// Start the server
await new Promise<void>((resolve, reject) => {
try {
// Leave this empty for now, smtpServer.start() is handled by the SMTPServer class internally
// The server is started when it's created
logger.log('info', `UnifiedEmailServer listening on port ${port}`);
// Event handlers are managed internally by the SmtpServer class
// No need to access the private server property
resolve();
} catch (err) {
if ((err as any).code === 'EADDRINUSE') {
logger.log('error', `Port ${port} is already in use`);
reject(new Error(`Port ${port} is already in use`));
} else {
logger.log('error', `Error starting server on port ${port}: ${err.message}`);
reject(err);
}
}
});
}
logger.log('info', 'UnifiedEmailServer started successfully');
this.emit('started');
} catch (error) {
logger.log('error', `Failed to start UnifiedEmailServer: ${error.message}`);
throw error;
}
}
/**
* Handle a socket from smartproxy in socket-handler mode
* @param socket The socket to handle
* @param port The port this connection is for (25, 587, 465)
*/
public async handleSocket(socket: plugins.net.Socket | plugins.tls.TLSSocket, port: number): Promise<void> {
if (!this.options.useSocketHandler) {
logger.log('error', 'handleSocket called but useSocketHandler is not enabled');
socket.destroy();
return;
}
logger.log('info', `Handling socket for port ${port}`);
// Create a temporary SMTP server instance for this connection
// We need a full server instance because the SMTP protocol handler needs all components
const smtpServerOptions = {
port,
hostname: this.options.hostname,
key: this.options.tls?.keyPath ? plugins.fs.readFileSync(this.options.tls.keyPath, 'utf8') : undefined,
cert: this.options.tls?.certPath ? plugins.fs.readFileSync(this.options.tls.certPath, 'utf8') : undefined
};
// Create the SMTP server instance
const smtpServer = createSmtpServer(this, smtpServerOptions);
// Get the connection manager from the server
const connectionManager = (smtpServer as any).connectionManager;
if (!connectionManager) {
logger.log('error', 'Could not get connection manager from SMTP server');
socket.destroy();
return;
}
// Determine if this is a secure connection
// Port 465 uses implicit TLS, so the socket is already secure
const isSecure = port === 465 || socket instanceof plugins.tls.TLSSocket;
// Pass the socket to the connection manager
try {
await connectionManager.handleConnection(socket, isSecure);
} catch (error) {
logger.log('error', `Error handling socket connection: ${error.message}`);
socket.destroy();
}
}
/**
* Stop the unified email server
*/
public async stop(): Promise<void> {
logger.log('info', 'Stopping UnifiedEmailServer');
try {
// Clear the servers array - servers will be garbage collected
this.servers = [];
// Stop the delivery system
if (this.deliverySystem) {
await this.deliverySystem.stop();
logger.log('info', 'Email delivery system stopped');
}
// Shut down the delivery queue
if (this.deliveryQueue) {
await this.deliveryQueue.shutdown();
logger.log('info', 'Email delivery queue shut down');
}
// Close all SMTP client connections
for (const [clientKey, client] of this.smtpClients) {
try {
await client.close();
logger.log('info', `Closed SMTP client pool for ${clientKey}`);
} catch (error) {
logger.log('warn', `Error closing SMTP client for ${clientKey}: ${error.message}`);
}
}
this.smtpClients.clear();
logger.log('info', 'UnifiedEmailServer stopped successfully');
this.emit('stopped');
} catch (error) {
logger.log('error', `Error stopping UnifiedEmailServer: ${error.message}`);
throw error;
}
}
/**
* Process email based on routing rules
*/
public async processEmailByMode(emailData: Email | Buffer, session: IExtendedSmtpSession): Promise<Email> {
// Convert Buffer to Email if needed
let email: Email;
if (Buffer.isBuffer(emailData)) {
// Parse the email data buffer into an Email object
try {
const parsed = await plugins.mailparser.simpleParser(emailData);
email = new Email({
from: parsed.from?.value[0]?.address || session.envelope.mailFrom.address,
to: session.envelope.rcptTo[0]?.address || '',
subject: parsed.subject || '',
text: parsed.text || '',
html: parsed.html || undefined,
attachments: parsed.attachments?.map(att => ({
filename: att.filename || '',
content: att.content,
contentType: att.contentType
})) || []
});
} catch (error) {
logger.log('error', `Error parsing email data: ${error.message}`);
throw new Error(`Error parsing email data: ${error.message}`);
}
} else {
email = emailData;
}
// First check if this is a bounce notification email
// Look for common bounce notification subject patterns
const subject = email.subject || '';
const isBounceLike = /mail delivery|delivery (failed|status|notification)|failure notice|returned mail|undeliverable|delivery problem/i.test(subject);
if (isBounceLike) {
logger.log('info', `Email subject matches bounce notification pattern: "${subject}"`);
// Try to process as a bounce
const isBounce = await this.processBounceNotification(email);
if (isBounce) {
logger.log('info', 'Successfully processed as bounce notification, skipping regular processing');
return email;
}
logger.log('info', 'Not a valid bounce notification, continuing with regular processing');
}
// Find matching route
const context: IEmailContext = { email, session };
const route = await this.emailRouter.evaluateRoutes(context);
if (!route) {
// No matching route - reject
throw new Error('No matching route for email');
}
// Store matched route in session
session.matchedRoute = route;
// Execute action based on route
await this.executeAction(route.action, email, context);
// Return the processed email
return email;
}
/**
* Execute action based on route configuration
*/
private async executeAction(action: IEmailAction, email: Email, context: IEmailContext): Promise<void> {
switch (action.type) {
case 'forward':
await this.handleForwardAction(action, email, context);
break;
case 'process':
await this.handleProcessAction(action, email, context);
break;
case 'deliver':
await this.handleDeliverAction(action, email, context);
break;
case 'reject':
await this.handleRejectAction(action, email, context);
break;
default:
throw new Error(`Unknown action type: ${(action as any).type}`);
}
}
/**
* Handle forward action
*/
private async handleForwardAction(_action: IEmailAction, email: Email, context: IEmailContext): Promise<void> {
if (!_action.forward) {
throw new Error('Forward action requires forward configuration');
}
const { host, port = 25, auth, addHeaders } = _action.forward;
logger.log('info', `Forwarding email to ${host}:${port}`);
// Add forwarding headers
if (addHeaders) {
for (const [key, value] of Object.entries(addHeaders)) {
email.headers[key] = value;
}
}
// Add standard forwarding headers
email.headers['X-Forwarded-For'] = context.session.remoteAddress || 'unknown';
email.headers['X-Forwarded-To'] = email.to.join(', ');
email.headers['X-Forwarded-Date'] = new Date().toISOString();
// Get SMTP client
const client = this.getSmtpClient(host, port);
try {
// Send email
await client.sendMail(email);
logger.log('info', `Successfully forwarded email to ${host}:${port}`);
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.INFO,
type: SecurityEventType.EMAIL_FORWARDING,
message: 'Email forwarded successfully',
ipAddress: context.session.remoteAddress,
details: {
sessionId: context.session.id,
routeName: context.session.matchedRoute?.name,
targetHost: host,
targetPort: port,
recipients: email.to
},
success: true
});
} catch (error) {
logger.log('error', `Failed to forward email: ${error.message}`);
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.ERROR,
type: SecurityEventType.EMAIL_FORWARDING,
message: 'Email forwarding failed',
ipAddress: context.session.remoteAddress,
details: {
sessionId: context.session.id,
routeName: context.session.matchedRoute?.name,
targetHost: host,
targetPort: port,
error: error.message
},
success: false
});
// Handle as bounce
for (const recipient of email.getAllRecipients()) {
await this.bounceManager.processSmtpFailure(recipient, error.message, {
sender: email.from,
originalEmailId: email.headers['Message-ID'] as string
});
}
throw error;
}
}
/**
* Handle process action
*/
private async handleProcessAction(action: IEmailAction, email: Email, context: IEmailContext): Promise<void> {
logger.log('info', `Processing email with action options`);
// Apply scanning if requested
if (action.process?.scan) {
// Use existing content scanner
// Note: ContentScanner integration would go here
logger.log('info', 'Content scanning requested');
}
// Note: DKIM signing will be applied at delivery time to ensure signature validity
// Queue for delivery
const queue = action.process?.queue || 'normal';
await this.deliveryQueue.enqueue(email, 'process', context.session.matchedRoute!);
logger.log('info', `Email queued for delivery in ${queue} queue`);
}
/**
* Handle deliver action
*/
private async handleDeliverAction(_action: IEmailAction, email: Email, context: IEmailContext): Promise<void> {
logger.log('info', `Delivering email locally`);
// Queue for local delivery
await this.deliveryQueue.enqueue(email, 'mta', context.session.matchedRoute!);
logger.log('info', 'Email queued for local delivery');
}
/**
* Handle reject action
*/
private async handleRejectAction(action: IEmailAction, email: Email, context: IEmailContext): Promise<void> {
const code = action.reject?.code || 550;
const message = action.reject?.message || 'Message rejected';
logger.log('info', `Rejecting email with code ${code}: ${message}`);
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.WARN,
type: SecurityEventType.EMAIL_PROCESSING,
message: 'Email rejected by routing rule',
ipAddress: context.session.remoteAddress,
details: {
sessionId: context.session.id,
routeName: context.session.matchedRoute?.name,
rejectCode: code,
rejectMessage: message,
from: email.from,
to: email.to
},
success: false
});
// Throw error with SMTP code and message
const error = new Error(message);
(error as any).responseCode = code;
throw error;
}
/**
* Handle email in MTA mode (programmatic processing)
*/
private async _handleMtaMode(email: Email, session: IExtendedSmtpSession): Promise<void> {
logger.log('info', `Handling email in MTA mode for session ${session.id}`);
try {
// Apply MTA rule options if provided
if (session.matchedRoute?.action.options?.mtaOptions) {
const options = session.matchedRoute.action.options.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}`);
try {
// Ensure DKIM keys exist for the domain
await this.dkimCreator.handleDKIMKeysForDomain(options.dkimOptions.domainName);
// Convert Email to raw format for signing
const rawEmail = email.toRFC822String();
// Create headers object
const headers = {};
for (const [key, value] of Object.entries(email.headers)) {
headers[key] = value;
}
// Sign the email
const signResult = await plugins.dkimSign(rawEmail, {
canonicalization: 'relaxed/relaxed',
algorithm: 'rsa-sha256',
signTime: new Date(),
signatureData: [
{
signingDomain: options.dkimOptions.domainName,
selector: options.dkimOptions.keySelector || 'mta',
privateKey: (await this.dkimCreator.readDKIMKeys(options.dkimOptions.domainName)).privateKey,
algorithm: 'rsa-sha256',
canonicalization: 'relaxed/relaxed'
}
]
});
// Add the DKIM-Signature header to the email
if (signResult.signatures) {
email.addHeader('DKIM-Signature', signResult.signatures);
logger.log('info', `Successfully added DKIM signature for ${options.dkimOptions.domainName}`);
}
} catch (error) {
logger.log('error', `Failed to sign email with DKIM: ${error.message}`);
}
}
}
// Get email content for logging/processing
const subject = email.subject;
const recipients = email.getAllRecipients().join(', ');
logger.log('info', `Email processed by MTA: ${subject} to ${recipients}`);
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.INFO,
type: SecurityEventType.EMAIL_PROCESSING,
message: 'Email processed by MTA',
ipAddress: session.remoteAddress,
details: {
sessionId: session.id,
ruleName: session.matchedRoute?.name || 'default',
subject,
recipients
},
success: true
});
} catch (error) {
logger.log('error', `Failed to process email in MTA mode: ${error.message}`);
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.ERROR,
type: SecurityEventType.EMAIL_PROCESSING,
message: 'MTA processing failed',
ipAddress: session.remoteAddress,
details: {
sessionId: session.id,
ruleName: session.matchedRoute?.name || 'default',
error: error.message
},
success: false
});
throw error;
}
}
/**
* Handle email in process mode (store-and-forward with scanning)
*/
private async _handleProcessMode(email: Email, session: IExtendedSmtpSession): Promise<void> {
logger.log('info', `Handling email in process mode for session ${session.id}`);
try {
const route = session.matchedRoute;
// Apply content scanning if enabled
if (route?.action.options?.contentScanning && route.action.options.scanners && route.action.options.scanners.length > 0) {
logger.log('info', 'Performing content scanning');
// Apply each scanner
for (const scanner of route.action.options.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 (route?.action.options?.transformations && route.action.options.transformations.length > 0) {
logger.log('info', 'Applying email transformations');
for (const transform of route.action.options.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`);
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.INFO,
type: SecurityEventType.EMAIL_PROCESSING,
message: 'Email processed and queued',
ipAddress: session.remoteAddress,
details: {
sessionId: session.id,
ruleName: route?.name || 'default',
contentScanning: route?.action.options?.contentScanning || false,
subject: email.subject
},
success: true
});
} catch (error) {
logger.log('error', `Failed to process email: ${error.message}`);
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.ERROR,
type: SecurityEventType.EMAIL_PROCESSING,
message: 'Email processing failed',
ipAddress: session.remoteAddress,
details: {
sessionId: session.id,
ruleName: session.matchedRoute?.name || 'default',
error: error.message
},
success: false
});
throw error;
}
}
/**
* Get file extension from filename
*/
private getFileExtension(filename: string): string {
return filename.substring(filename.lastIndexOf('.')).toLowerCase();
}
/**
* Set up automatic DKIM configuration with DNS server
*/
private async setupAutomaticDkim(): Promise<void> {
if (!this.options.domains || this.options.domains.length === 0) {
logger.log('warn', 'No domains configured for DKIM');
return;
}
const selector = this.options.dkim?.selector || 'default';
for (const domain of this.options.domains) {
try {
// Check if DKIM keys already exist for this domain
let keyPair: { privateKey: string; publicKey: string };
try {
// Try to read existing keys
keyPair = await this.dkimCreator.readDKIMKeys(domain);
logger.log('info', `Using existing DKIM keys for domain: ${domain}`);
} catch (error) {
// Generate new keys if they don't exist
keyPair = await this.dkimCreator.createDKIMKeys();
// Store them for future use
await this.dkimCreator.createAndStoreDKIMKeys(domain);
logger.log('info', `Generated new DKIM keys for domain: ${domain}`);
}
// Store the private key for signing
this.dkimKeys.set(domain, keyPair.privateKey);
// Extract the public key for DNS
const publicKeyBase64 = keyPair.publicKey
.replace(/-----BEGIN PUBLIC KEY-----/g, '')
.replace(/-----END PUBLIC KEY-----/g, '')
.replace(/\s/g, '');
// Register DNS handler for this domain's DKIM records
this.dcRouter.dnsServer.registerHandler(
`${selector}._domainkey.${domain}`,
['TXT'],
() => ({
name: `${selector}._domainkey.${domain}`,
type: 'TXT',
class: 'IN',
ttl: 300,
data: `v=DKIM1; k=rsa; p=${publicKeyBase64}`
})
);
logger.log('info', `DKIM DNS handler registered for domain: ${domain} with selector: ${selector}`);
} catch (error) {
logger.log('error', `Failed to set up DKIM for domain ${domain}: ${error.message}`);
}
}
}
/**
* Generate SmartProxy routes for email ports
*/
public generateProxyRoutes(portMapping?: Record<number, number>): any[] {
const routes: any[] = [];
const defaultPortMapping = {
25: 10025,
587: 10587,
465: 10465
};
const actualPortMapping = portMapping || defaultPortMapping;
// Generate routes for each configured port
for (const externalPort of this.options.ports) {
const internalPort = actualPortMapping[externalPort] || externalPort + 10000;
let routeName = 'email-route';
let tlsMode = 'passthrough';
// Configure based on port
switch (externalPort) {
case 25:
routeName = 'smtp-route';
tlsMode = 'passthrough'; // STARTTLS
break;
case 587:
routeName = 'submission-route';
tlsMode = 'passthrough'; // STARTTLS
break;
case 465:
routeName = 'smtps-route';
tlsMode = 'terminate'; // Implicit TLS
break;
default:
routeName = `email-port-${externalPort}-route`;
}
routes.push({
name: routeName,
match: {
ports: [externalPort]
},
action: {
type: 'forward',
target: {
host: 'localhost',
port: internalPort
},
tls: {
mode: tlsMode
}
}
});
}
return routes;
}
/**
* Update server configuration
*/
public updateOptions(options: Partial<IUnifiedEmailServerOptions>): void {
// Stop the server if changing ports
const portsChanged = options.ports &&
(!this.options.ports ||
JSON.stringify(options.ports) !== JSON.stringify(this.options.ports));
if (portsChanged) {
this.stop().then(() => {
this.options = { ...this.options, ...options };
this.start();
});
} else {
// Update options without restart
this.options = { ...this.options, ...options };
// Update email router if routes changed
if (options.routes) {
this.emailRouter.updateRoutes(options.routes);
}
}
}
/**
* Update email routes
*/
public updateEmailRoutes(routes: IEmailRoute[]): void {
this.options.routes = routes;
this.emailRouter.updateRoutes(routes);
}
/**
* Get server statistics
*/
public getStats(): IServerStats {
return { ...this.stats };
}
/**
* Update email routes dynamically
*/
public updateRoutes(routes: IEmailRoute[]): void {
this.emailRouter.setRoutes(routes);
logger.log('info', `Updated email routes with ${routes.length} routes`);
}
/**
* Send an email through the delivery system
* @param email The email to send
* @param mode The processing mode to use
* @param rule Optional rule to apply
* @param options Optional sending options
* @returns The ID of the queued email
*/
public async sendEmail(
email: Email,
mode: EmailProcessingMode = 'mta',
route?: IEmailRoute,
options?: {
skipSuppressionCheck?: boolean;
ipAddress?: string;
isTransactional?: boolean;
}
): Promise<string> {
logger.log('info', `Sending email: ${email.subject} to ${email.to.join(', ')}`);
try {
// Validate the email
if (!email.from) {
throw new Error('Email must have a sender address');
}
if (!email.to || email.to.length === 0) {
throw new Error('Email must have at least one recipient');
}
// Check if any recipients are on the suppression list (unless explicitly skipped)
if (!options?.skipSuppressionCheck) {
const suppressedRecipients = email.to.filter(recipient => this.isEmailSuppressed(recipient));
if (suppressedRecipients.length > 0) {
// Filter out suppressed recipients
const originalCount = email.to.length;
const suppressed = suppressedRecipients.map(recipient => {
const info = this.getSuppressionInfo(recipient);
return {
email: recipient,
reason: info?.reason || 'Unknown',
until: info?.expiresAt ? new Date(info.expiresAt).toISOString() : 'permanent'
};
});
logger.log('warn', `Filtering out ${suppressedRecipients.length} suppressed recipient(s)`, { suppressed });
// If all recipients are suppressed, throw an error
if (suppressedRecipients.length === originalCount) {
throw new Error('All recipients are on the suppression list');
}
// Filter the recipients list to only include non-suppressed addresses
email.to = email.to.filter(recipient => !this.isEmailSuppressed(recipient));
}
}
// IP warmup handling
let ipAddress = options?.ipAddress;
// If no specific IP was provided, use IP warmup manager to find the best IP
if (!ipAddress) {
const domain = email.from.split('@')[1];
ipAddress = this.getBestIPForSending({
from: email.from,
to: email.to,
domain,
isTransactional: options?.isTransactional
});
if (ipAddress) {
logger.log('info', `Selected IP ${ipAddress} for sending based on warmup status`);
}
}
// If an IP is provided or selected by warmup manager, check its capacity
if (ipAddress) {
// Check if the IP can send more today
if (!this.canIPSendMoreToday(ipAddress)) {
logger.log('warn', `IP ${ipAddress} has reached its daily sending limit, email will be queued for later delivery`);
}
// Check if the IP can send more this hour
if (!this.canIPSendMoreThisHour(ipAddress)) {
logger.log('warn', `IP ${ipAddress} has reached its hourly sending limit, email will be queued for later delivery`);
}
// Record the send for IP warmup tracking
this.recordIPSend(ipAddress);
// Add IP header to the email
email.addHeader('X-Sending-IP', ipAddress);
}
// Check if the sender domain has DKIM keys and sign the email if needed
if (mode === 'mta' && route?.action.options?.mtaOptions?.dkimSign) {
const domain = email.from.split('@')[1];
await this.handleDkimSigning(email, domain, route.action.options.mtaOptions.dkimOptions?.keySelector || 'mta');
}
// Generate a unique ID for this email
const id = plugins.uuid.v4();
// Queue the email for delivery
await this.deliveryQueue.enqueue(email, mode, route);
// Record 'sent' event for domain reputation monitoring
const senderDomain = email.from.split('@')[1];
if (senderDomain) {
this.recordReputationEvent(senderDomain, {
type: 'sent',
count: email.to.length
});
}
logger.log('info', `Email queued with ID: ${id}`);
return id;
} catch (error) {
logger.log('error', `Failed to send email: ${error.message}`);
throw error;
}
}
/**
* Handle DKIM signing for an email
* @param email The email to sign
* @param domain The domain to sign with
* @param selector The DKIM selector
*/
private async handleDkimSigning(email: Email, domain: string, selector: string): Promise<void> {
try {
// Ensure we have DKIM keys for this domain
await this.dkimCreator.handleDKIMKeysForDomain(domain);
// Get the private key
const { privateKey } = await this.dkimCreator.readDKIMKeys(domain);
// Convert Email to raw format for signing
const rawEmail = email.toRFC822String();
// Sign the email
const signResult = await plugins.dkimSign(rawEmail, {
canonicalization: 'relaxed/relaxed',
algorithm: 'rsa-sha256',
signTime: new Date(),
signatureData: [
{
signingDomain: domain,
selector: selector,
privateKey: privateKey,
algorithm: 'rsa-sha256',
canonicalization: 'relaxed/relaxed'
}
]
});
// Add the DKIM-Signature header to the email
if (signResult.signatures) {
email.addHeader('DKIM-Signature', signResult.signatures);
logger.log('info', `Successfully added DKIM signature for ${domain}`);
}
} catch (error) {
logger.log('error', `Failed to sign email with DKIM: ${error.message}`);
// Continue without DKIM rather than failing the send
}
}
/**
* Process a bounce notification email
* @param bounceEmail The email containing bounce notification information
* @returns Processed bounce record or null if not a bounce
*/
public async processBounceNotification(bounceEmail: Email): Promise<boolean> {
logger.log('info', 'Processing potential bounce notification email');
try {
// Process as a bounce notification (no conversion needed anymore)
const bounceRecord = await this.bounceManager.processBounceEmail(bounceEmail);
if (bounceRecord) {
logger.log('info', `Successfully processed bounce notification for ${bounceRecord.recipient}`, {
bounceType: bounceRecord.bounceType,
bounceCategory: bounceRecord.bounceCategory
});
// Notify any registered listeners about the bounce
this.emit('bounceProcessed', bounceRecord);
// Record bounce event for domain reputation tracking
if (bounceRecord.domain) {
this.recordReputationEvent(bounceRecord.domain, {
type: 'bounce',
hardBounce: bounceRecord.bounceCategory === BounceCategory.HARD,
receivingDomain: bounceRecord.recipient.split('@')[1]
});
}
// Log security event
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.INFO,
type: SecurityEventType.EMAIL_VALIDATION,
message: `Bounce notification processed for recipient`,
domain: bounceRecord.domain,
details: {
recipient: bounceRecord.recipient,
bounceType: bounceRecord.bounceType,
bounceCategory: bounceRecord.bounceCategory
},
success: true
});
return true;
} else {
logger.log('info', 'Email not recognized as a bounce notification');
return false;
}
} catch (error) {
logger.log('error', `Error processing bounce notification: ${error.message}`);
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.ERROR,
type: SecurityEventType.EMAIL_VALIDATION,
message: 'Failed to process bounce notification',
details: {
error: error.message,
subject: bounceEmail.subject
},
success: false
});
return false;
}
}
/**
* Process an SMTP failure as a bounce
* @param recipient Recipient email that failed
* @param smtpResponse SMTP error response
* @param options Additional options for bounce processing
* @returns Processed bounce record
*/
public async processSmtpFailure(
recipient: string,
smtpResponse: string,
options: {
sender?: string;
originalEmailId?: string;
statusCode?: string;
headers?: Record<string, string>;
} = {}
): Promise<boolean> {
logger.log('info', `Processing SMTP failure for ${recipient}: ${smtpResponse}`);
try {
// Process the SMTP failure through the bounce manager
const bounceRecord = await this.bounceManager.processSmtpFailure(
recipient,
smtpResponse,
options
);
logger.log('info', `Successfully processed SMTP failure for ${recipient} as ${bounceRecord.bounceCategory} bounce`, {
bounceType: bounceRecord.bounceType
});
// Notify any registered listeners about the bounce
this.emit('bounceProcessed', bounceRecord);
// Record bounce event for domain reputation tracking
if (bounceRecord.domain) {
this.recordReputationEvent(bounceRecord.domain, {
type: 'bounce',
hardBounce: bounceRecord.bounceCategory === BounceCategory.HARD,
receivingDomain: bounceRecord.recipient.split('@')[1]
});
}
// Log security event
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.INFO,
type: SecurityEventType.EMAIL_VALIDATION,
message: `SMTP failure processed for recipient`,
domain: bounceRecord.domain,
details: {
recipient: bounceRecord.recipient,
bounceType: bounceRecord.bounceType,
bounceCategory: bounceRecord.bounceCategory,
smtpResponse
},
success: true
});
return true;
} catch (error) {
logger.log('error', `Error processing SMTP failure: ${error.message}`);
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.ERROR,
type: SecurityEventType.EMAIL_VALIDATION,
message: 'Failed to process SMTP failure',
details: {
recipient,
smtpResponse,
error: error.message
},
success: false
});
return false;
}
}
/**
* Check if an email address is suppressed (has bounced previously)
* @param email Email address to check
* @returns Whether the email is suppressed
*/
public isEmailSuppressed(email: string): boolean {
return this.bounceManager.isEmailSuppressed(email);
}
/**
* Get suppression information for an email
* @param email Email address to check
* @returns Suppression information or null if not suppressed
*/
public getSuppressionInfo(email: string): {
reason: string;
timestamp: number;
expiresAt?: number;
} | null {
return this.bounceManager.getSuppressionInfo(email);
}
/**
* Get bounce history information for an email
* @param email Email address to check
* @returns Bounce history or null if no bounces
*/
public getBounceHistory(email: string): {
lastBounce: number;
count: number;
type: BounceType;
category: BounceCategory;
} | null {
return this.bounceManager.getBounceInfo(email);
}
/**
* Get all suppressed email addresses
* @returns Array of suppressed email addresses
*/
public getSuppressionList(): string[] {
return this.bounceManager.getSuppressionList();
}
/**
* Get all hard bounced email addresses
* @returns Array of hard bounced email addresses
*/
public getHardBouncedAddresses(): string[] {
return this.bounceManager.getHardBouncedAddresses();
}
/**
* Add an email to the suppression list
* @param email Email address to suppress
* @param reason Reason for suppression
* @param expiresAt Optional expiration time (undefined for permanent)
*/
public addToSuppressionList(email: string, reason: string, expiresAt?: number): void {
this.bounceManager.addToSuppressionList(email, reason, expiresAt);
logger.log('info', `Added ${email} to suppression list: ${reason}`);
}
/**
* Remove an email from the suppression list
* @param email Email address to remove from suppression
*/
public removeFromSuppressionList(email: string): void {
this.bounceManager.removeFromSuppressionList(email);
logger.log('info', `Removed ${email} from suppression list`);
}
/**
* Get the status of IP warmup process
* @param ipAddress Optional specific IP to check
* @returns Status of IP warmup
*/
public getIPWarmupStatus(ipAddress?: string): any {
return this.ipWarmupManager.getWarmupStatus(ipAddress);
}
/**
* Add a new IP address to the warmup process
* @param ipAddress IP address to add
*/
public addIPToWarmup(ipAddress: string): void {
this.ipWarmupManager.addIPToWarmup(ipAddress);
}
/**
* Remove an IP address from the warmup process
* @param ipAddress IP address to remove
*/
public removeIPFromWarmup(ipAddress: string): void {
this.ipWarmupManager.removeIPFromWarmup(ipAddress);
}
/**
* Update metrics for an IP in the warmup process
* @param ipAddress IP address
* @param metrics Metrics to update
*/
public updateIPWarmupMetrics(
ipAddress: string,
metrics: { openRate?: number; bounceRate?: number; complaintRate?: number }
): void {
this.ipWarmupManager.updateMetrics(ipAddress, metrics);
}
/**
* Check if an IP can send more emails today
* @param ipAddress IP address to check
* @returns Whether the IP can send more today
*/
public canIPSendMoreToday(ipAddress: string): boolean {
return this.ipWarmupManager.canSendMoreToday(ipAddress);
}
/**
* Check if an IP can send more emails in the current hour
* @param ipAddress IP address to check
* @returns Whether the IP can send more this hour
*/
public canIPSendMoreThisHour(ipAddress: string): boolean {
return this.ipWarmupManager.canSendMoreThisHour(ipAddress);
}
/**
* Get the best IP to use for sending an email based on warmup status
* @param emailInfo Information about the email being sent
* @returns Best IP to use or null
*/
public getBestIPForSending(emailInfo: {
from: string;
to: string[];
domain: string;
isTransactional?: boolean;
}): string | null {
return this.ipWarmupManager.getBestIPForSending(emailInfo);
}
/**
* Set the active IP allocation policy for warmup
* @param policyName Name of the policy to set
*/
public setIPAllocationPolicy(policyName: string): void {
this.ipWarmupManager.setActiveAllocationPolicy(policyName);
}
/**
* Record that an email was sent using a specific IP
* @param ipAddress IP address used for sending
*/
public recordIPSend(ipAddress: string): void {
this.ipWarmupManager.recordSend(ipAddress);
}
/**
* Get reputation data for a domain
* @param domain Domain to get reputation for
* @returns Domain reputation metrics
*/
public getDomainReputationData(domain: string): any {
return this.senderReputationMonitor.getReputationData(domain);
}
/**
* Get summary reputation data for all monitored domains
* @returns Summary data for all domains
*/
public getReputationSummary(): any {
return this.senderReputationMonitor.getReputationSummary();
}
/**
* Add a domain to the reputation monitoring system
* @param domain Domain to add
*/
public addDomainToMonitoring(domain: string): void {
this.senderReputationMonitor.addDomain(domain);
}
/**
* Remove a domain from the reputation monitoring system
* @param domain Domain to remove
*/
public removeDomainFromMonitoring(domain: string): void {
this.senderReputationMonitor.removeDomain(domain);
}
/**
* Record an email event for domain reputation tracking
* @param domain Domain sending the email
* @param event Event details
*/
public recordReputationEvent(domain: string, event: {
type: 'sent' | 'delivered' | 'bounce' | 'complaint' | 'open' | 'click';
count?: number;
hardBounce?: boolean;
receivingDomain?: string;
}): void {
this.senderReputationMonitor.recordSendEvent(domain, event);
}
/**
* Check if DKIM key exists for a domain
* @param domain Domain to check
*/
public hasDkimKey(domain: string): boolean {
return this.dkimKeys.has(domain);
}
/**
* Record successful email delivery
* @param domain Sending domain
*/
public recordDelivery(domain: string): void {
this.recordReputationEvent(domain, {
type: 'delivered',
count: 1
});
}
/**
* Record email bounce
* @param domain Sending domain
* @param receivingDomain Receiving domain that bounced
* @param bounceType Type of bounce (hard/soft)
* @param reason Bounce reason
*/
public recordBounce(domain: string, receivingDomain: string, bounceType: 'hard' | 'soft', reason: string): void {
// Record bounce in bounce manager
const bounceRecord = {
id: `bounce_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
recipient: `user@${receivingDomain}`,
sender: `user@${domain}`,
domain: domain,
bounceType: bounceType === 'hard' ? BounceType.INVALID_RECIPIENT : BounceType.TEMPORARY_FAILURE,
bounceCategory: bounceType === 'hard' ? BounceCategory.HARD : BounceCategory.SOFT,
timestamp: Date.now(),
smtpResponse: reason,
diagnosticCode: reason,
statusCode: bounceType === 'hard' ? '550' : '450',
processed: false
};
// Process the bounce
this.bounceManager.processBounce(bounceRecord);
// Record reputation event
this.recordReputationEvent(domain, {
type: 'bounce',
count: 1,
hardBounce: bounceType === 'hard',
receivingDomain
});
}
}