1469 lines
116 KiB
JavaScript
1469 lines
116 KiB
JavaScript
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 { EmailRouter } from './classes.email.router.js';
|
|
import { Email } from '../core/classes.email.js';
|
|
import { DomainRegistry } from './classes.domain.registry.js';
|
|
import { DnsManager } from './classes.dns.manager.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 { MultiModeDeliverySystem } from '../delivery/classes.delivery.system.js';
|
|
import { UnifiedDeliveryQueue } from '../delivery/classes.delivery.queue.js';
|
|
import { UnifiedRateLimiter } from '../delivery/classes.unified.rate.limiter.js';
|
|
import { SmtpState } from '../delivery/interfaces.js';
|
|
/**
|
|
* Unified email server that handles all email traffic with pattern-based routing
|
|
*/
|
|
export class UnifiedEmailServer extends EventEmitter {
|
|
dcRouter;
|
|
options;
|
|
emailRouter;
|
|
domainRegistry;
|
|
servers = [];
|
|
stats;
|
|
// Add components needed for sending and securing emails
|
|
dkimCreator;
|
|
ipReputationChecker; // TODO: Implement IP reputation checks in processEmailByMode
|
|
bounceManager;
|
|
ipWarmupManager;
|
|
senderReputationMonitor;
|
|
deliveryQueue;
|
|
deliverySystem;
|
|
rateLimiter; // TODO: Implement rate limiting in SMTP server handlers
|
|
dkimKeys = new Map(); // domain -> private key
|
|
smtpClients = new Map(); // host:port -> client
|
|
constructor(dcRouter, options) {
|
|
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 with storage manager
|
|
this.dkimCreator = new DKIMCreator(paths.keysDir, dcRouter.storageManager);
|
|
// Initialize IP reputation checker with storage manager
|
|
this.ipReputationChecker = IPReputationChecker.getInstance({
|
|
enableLocalCache: true,
|
|
enableDNSBL: true,
|
|
enableIPInfo: true
|
|
}, dcRouter.storageManager);
|
|
// Initialize bounce manager with storage manager
|
|
this.bounceManager = new BounceManager({
|
|
maxCacheSize: 10000,
|
|
cacheTTL: 30 * 24 * 60 * 60 * 1000, // 30 days
|
|
storageManager: dcRouter.storageManager
|
|
});
|
|
// IP warmup manager and sender reputation monitor are optional
|
|
// They will be initialized when the deliverability module is available
|
|
this.ipWarmupManager = null;
|
|
this.senderReputationMonitor = null;
|
|
// Initialize domain registry
|
|
this.domainRegistry = new DomainRegistry(options.domains, options.defaults);
|
|
// Initialize email router with routes and storage manager
|
|
this.emailRouter = new EmailRouter(options.routes || [], {
|
|
storageManager: dcRouter.storageManager,
|
|
persistChanges: true
|
|
});
|
|
// 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 = {
|
|
storageType: 'memory', // Default to memory storage
|
|
maxRetries: 3,
|
|
baseRetryDelay: 300000, // 5 minutes
|
|
maxRetryDelay: 3600000 // 1 hour
|
|
};
|
|
this.deliveryQueue = new UnifiedDeliveryQueue(queueOptions);
|
|
const deliveryOptions = {
|
|
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;
|
|
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
|
|
*/
|
|
getSmtpClient(host, port = 25) {
|
|
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
|
|
*/
|
|
async start() {
|
|
logger.log('info', `Starting UnifiedEmailServer on ports: ${this.options.ports.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 DKIM for all domains
|
|
await this.setupDkimForDomains();
|
|
logger.log('info', 'DKIM configuration completed for all domains');
|
|
// Create DNS manager and ensure all DNS records are created
|
|
const dnsManager = new DnsManager(this.dcRouter);
|
|
await dnsManager.ensureDnsRecords(this.domainRegistry.getAllConfigs(), this.dkimCreator);
|
|
logger.log('info', 'DNS records ensured for all configured domains');
|
|
// Apply per-domain rate limits
|
|
this.applyDomainRateLimits();
|
|
logger.log('info', 'Per-domain rate limits configured');
|
|
// Check and rotate DKIM keys if needed
|
|
await this.checkAndRotateDkimKeys();
|
|
logger.log('info', 'DKIM key rotation check 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;
|
|
let cert;
|
|
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) {
|
|
// 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) => {
|
|
// 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, serverOptions);
|
|
this.servers.push(smtpServer);
|
|
// Start the server
|
|
await new Promise((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.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)
|
|
*/
|
|
async handleSocket(socket, port) {
|
|
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.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
|
|
*/
|
|
async stop() {
|
|
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
|
|
*/
|
|
async processEmailByMode(emailData, session) {
|
|
// Convert Buffer to Email if needed
|
|
let 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 = { 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
|
|
*/
|
|
async executeAction(action, email, context) {
|
|
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.type}`);
|
|
}
|
|
}
|
|
/**
|
|
* Handle forward action
|
|
*/
|
|
async handleForwardAction(_action, email, context) {
|
|
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']
|
|
});
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
/**
|
|
* Handle process action
|
|
*/
|
|
async handleProcessAction(action, email, context) {
|
|
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
|
|
*/
|
|
async handleDeliverAction(_action, email, context) {
|
|
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
|
|
*/
|
|
async handleRejectAction(action, email, context) {
|
|
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.responseCode = code;
|
|
throw error;
|
|
}
|
|
/**
|
|
* Handle email in MTA mode (programmatic processing)
|
|
*/
|
|
async _handleMtaMode(email, session) {
|
|
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 dkimDomain = options.dkimOptions.domainName;
|
|
const dkimSelector = options.dkimOptions.keySelector || 'mta';
|
|
const dkimPrivateKey = (await this.dkimCreator.readDKIMKeys(dkimDomain)).privateKey;
|
|
const signResult = await plugins.dkimSign(rawEmail, {
|
|
signingDomain: dkimDomain,
|
|
selector: dkimSelector,
|
|
privateKey: dkimPrivateKey,
|
|
canonicalization: 'relaxed/relaxed',
|
|
algorithm: 'rsa-sha256',
|
|
signTime: new Date(),
|
|
signatureData: [
|
|
{
|
|
signingDomain: dkimDomain,
|
|
selector: dkimSelector,
|
|
privateKey: dkimPrivateKey,
|
|
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)
|
|
*/
|
|
async _handleProcessMode(email, session) {
|
|
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
|
|
*/
|
|
getFileExtension(filename) {
|
|
return filename.substring(filename.lastIndexOf('.')).toLowerCase();
|
|
}
|
|
/**
|
|
* Set up DKIM configuration for all domains
|
|
*/
|
|
async setupDkimForDomains() {
|
|
const domainConfigs = this.domainRegistry.getAllConfigs();
|
|
if (domainConfigs.length === 0) {
|
|
logger.log('warn', 'No domains configured for DKIM');
|
|
return;
|
|
}
|
|
for (const domainConfig of domainConfigs) {
|
|
const domain = domainConfig.domain;
|
|
const selector = domainConfig.dkim?.selector || 'default';
|
|
try {
|
|
// Check if DKIM keys already exist for this domain
|
|
let keyPair;
|
|
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);
|
|
// DNS record creation is now handled by DnsManager
|
|
logger.log('info', `DKIM keys loaded for domain: ${domain} with selector: ${selector}`);
|
|
}
|
|
catch (error) {
|
|
logger.log('error', `Failed to set up DKIM for domain ${domain}: ${error.message}`);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Apply per-domain rate limits from domain configurations
|
|
*/
|
|
applyDomainRateLimits() {
|
|
const domainConfigs = this.domainRegistry.getAllConfigs();
|
|
for (const domainConfig of domainConfigs) {
|
|
if (domainConfig.rateLimits) {
|
|
const domain = domainConfig.domain;
|
|
const rateLimitConfig = {};
|
|
// Convert domain-specific rate limits to the format expected by UnifiedRateLimiter
|
|
if (domainConfig.rateLimits.outbound) {
|
|
if (domainConfig.rateLimits.outbound.messagesPerMinute) {
|
|
rateLimitConfig.maxMessagesPerMinute = domainConfig.rateLimits.outbound.messagesPerMinute;
|
|
}
|
|
// Note: messagesPerHour and messagesPerDay would need additional implementation in rate limiter
|
|
}
|
|
if (domainConfig.rateLimits.inbound) {
|
|
if (domainConfig.rateLimits.inbound.messagesPerMinute) {
|
|
rateLimitConfig.maxMessagesPerMinute = domainConfig.rateLimits.inbound.messagesPerMinute;
|
|
}
|
|
if (domainConfig.rateLimits.inbound.connectionsPerIp) {
|
|
rateLimitConfig.maxConnectionsPerIP = domainConfig.rateLimits.inbound.connectionsPerIp;
|
|
}
|
|
if (domainConfig.rateLimits.inbound.recipientsPerMessage) {
|
|
rateLimitConfig.maxRecipientsPerMessage = domainConfig.rateLimits.inbound.recipientsPerMessage;
|
|
}
|
|
}
|
|
// Apply the rate limits if we have any
|
|
if (Object.keys(rateLimitConfig).length > 0) {
|
|
this.rateLimiter.applyDomainLimits(domain, rateLimitConfig);
|
|
logger.log('info', `Applied rate limits for domain ${domain}:`, rateLimitConfig);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Check and rotate DKIM keys if needed
|
|
*/
|
|
async checkAndRotateDkimKeys() {
|
|
const domainConfigs = this.domainRegistry.getAllConfigs();
|
|
for (const domainConfig of domainConfigs) {
|
|
const domain = domainConfig.domain;
|
|
const selector = domainConfig.dkim?.selector || 'default';
|
|
const rotateKeys = domainConfig.dkim?.rotateKeys || false;
|
|
const rotationInterval = domainConfig.dkim?.rotationInterval || 90;
|
|
const keySize = domainConfig.dkim?.keySize || 2048;
|
|
if (!rotateKeys) {
|
|
logger.log('debug', `DKIM key rotation disabled for ${domain}`);
|
|
continue;
|
|
}
|
|
try {
|
|
// Check if keys need rotation
|
|
const needsRotation = await this.dkimCreator.needsRotation(domain, selector, rotationInterval);
|
|
if (needsRotation) {
|
|
logger.log('info', `DKIM keys need rotation for ${domain} (selector: ${selector})`);
|
|
// Rotate the keys
|
|
const newSelector = await this.dkimCreator.rotateDkimKeys(domain, selector, keySize);
|
|
// Update the domain config with new selector
|
|
domainConfig.dkim = {
|
|
...domainConfig.dkim,
|
|
selector: newSelector
|
|
};
|
|
// Re-register DNS handler for new selector if internal-dns mode
|
|
if (domainConfig.dnsMode === 'internal-dns' && this.dcRouter.dnsServer) {
|
|
// Get new public key
|
|
const keyPair = await this.dkimCreator.readDKIMKeysForSelector(domain, newSelector);
|
|
const publicKeyBase64 = keyPair.publicKey
|
|
.replace(/-----BEGIN PUBLIC KEY-----/g, '')
|
|
.replace(/-----END PUBLIC KEY-----/g, '')
|
|
.replace(/\s/g, '');
|
|
const ttl = domainConfig.dns?.internal?.ttl || 3600;
|
|
// Register new selector
|
|
this.dcRouter.dnsServer.registerHandler(`${newSelector}._domainkey.${domain}`, ['TXT'], () => ({
|
|
name: `${newSelector}._domainkey.${domain}`,
|
|
type: 'TXT',
|
|
class: 'IN',
|
|
ttl: ttl,
|
|
data: `v=DKIM1; k=rsa; p=${publicKeyBase64}`
|
|
}));
|
|
logger.log('info', `DKIM DNS handler registered for new selector: ${newSelector}._domainkey.${domain}`);
|
|
// Store the updated public key in storage
|
|
await this.dcRouter.storageManager.set(`/email/dkim/${domain}/public.key`, keyPair.publicKey);
|
|
}
|
|
// Clean up old keys after grace period (async, don't wait)
|
|
this.dkimCreator.cleanupOldKeys(domain, 30).catch(error => {
|
|
logger.log('warn', `Failed to cleanup old DKIM keys for ${domain}: ${error.message}`);
|
|
});
|
|
}
|
|
else {
|
|
logger.log('debug', `DKIM keys for ${domain} are up to date`);
|
|
}
|
|
}
|
|
catch (error) {
|
|
logger.log('error', `Failed to check/rotate DKIM keys for ${domain}: ${error.message}`);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Generate SmartProxy routes for email ports
|
|
*/
|
|
generateProxyRoutes(portMapping) {
|
|
const routes = [];
|
|
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
|
|
*/
|
|
updateOptions(options) {
|
|
// 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 domain registry if domains changed
|
|
if (options.domains) {
|
|
this.domainRegistry = new DomainRegistry(options.domains, options.defaults || this.options.defaults);
|
|
}
|
|
// Update email router if routes changed
|
|
if (options.routes) {
|
|
this.emailRouter.updateRoutes(options.routes);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Update email routes
|
|
*/
|
|
updateEmailRoutes(routes) {
|
|
this.options.routes = routes;
|
|
this.emailRouter.updateRoutes(routes);
|
|
}
|
|
/**
|
|
* Get server statistics
|
|
*/
|
|
getStats() {
|
|
return { ...this.stats };
|
|
}
|
|
/**
|
|
* Get domain registry
|
|
*/
|
|
getDomainRegistry() {
|
|
return this.domainRegistry;
|
|
}
|
|
/**
|
|
* Update email routes dynamically
|
|
*/
|
|
updateRoutes(routes) {
|
|
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
|
|
*/
|
|
async sendEmail(email, mode = 'mta', route, options) {
|
|
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
|
|
*/
|
|
async handleDkimSigning(email, domain, selector) {
|
|
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, {
|
|
signingDomain: domain,
|
|
selector: selector,
|
|
privateKey: privateKey,
|
|
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
|
|
*/
|
|
async processBounceNotification(bounceEmail) {
|
|
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
|
|
*/
|
|
async processSmtpFailure(recipient, smtpResponse, options = {}) {
|
|
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
|
|
*/
|
|
isEmailSuppressed(email) {
|
|
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
|
|
*/
|
|
getSuppressionInfo(email) {
|
|
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
|
|
*/
|
|
getBounceHistory(email) {
|
|
return this.bounceManager.getBounceInfo(email);
|
|
}
|
|
/**
|
|
* Get all suppressed email addresses
|
|
* @returns Array of suppressed email addresses
|
|
*/
|
|
getSuppressionList() {
|
|
return this.bounceManager.getSuppressionList();
|
|
}
|
|
/**
|
|
* Get all hard bounced email addresses
|
|
* @returns Array of hard bounced email addresses
|
|
*/
|
|
getHardBouncedAddresses() {
|
|
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)
|
|
*/
|
|
addToSuppressionList(email, reason, expiresAt) {
|
|
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
|
|
*/
|
|
removeFromSuppressionList(email) {
|
|
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
|
|
*/
|
|
getIPWarmupStatus(ipAddress) {
|
|
return this.ipWarmupManager.getWarmupStatus(ipAddress);
|
|
}
|
|
/**
|
|
* Add a new IP address to the warmup process
|
|
* @param ipAddress IP address to add
|
|
*/
|
|
addIPToWarmup(ipAddress) {
|
|
this.ipWarmupManager.addIPToWarmup(ipAddress);
|
|
}
|
|
/**
|
|
* Remove an IP address from the warmup process
|
|
* @param ipAddress IP address to remove
|
|
*/
|
|
removeIPFromWarmup(ipAddress) {
|
|
this.ipWarmupManager.removeIPFromWarmup(ipAddress);
|
|
}
|
|
/**
|
|
* Update metrics for an IP in the warmup process
|
|
* @param ipAddress IP address
|
|
* @param metrics Metrics to update
|
|
*/
|
|
updateIPWarmupMetrics(ipAddress, metrics) {
|
|
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
|
|
*/
|
|
canIPSendMoreToday(ipAddress) {
|
|
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
|
|
*/
|
|
canIPSendMoreThisHour(ipAddress) {
|
|
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
|
|
*/
|
|
getBestIPForSending(emailInfo) {
|
|
return this.ipWarmupManager.getBestIPForSending(emailInfo);
|
|
}
|
|
/**
|
|
* Set the active IP allocation policy for warmup
|
|
* @param policyName Name of the policy to set
|
|
*/
|
|
setIPAllocationPolicy(policyName) {
|
|
this.ipWarmupManager.setActiveAllocationPolicy(policyName);
|
|
}
|
|
/**
|
|
* Record that an email was sent using a specific IP
|
|
* @param ipAddress IP address used for sending
|
|
*/
|
|
recordIPSend(ipAddress) {
|
|
this.ipWarmupManager.recordSend(ipAddress);
|
|
}
|
|
/**
|
|
* Get reputation data for a domain
|
|
* @param domain Domain to get reputation for
|
|
* @returns Domain reputation metrics
|
|
*/
|
|
getDomainReputationData(domain) {
|
|
return this.senderReputationMonitor.getReputationData(domain);
|
|
}
|
|
/**
|
|
* Get summary reputation data for all monitored domains
|
|
* @returns Summary data for all domains
|
|
*/
|
|
getReputationSummary() {
|
|
return this.senderReputationMonitor.getReputationSummary();
|
|
}
|
|
/**
|
|
* Add a domain to the reputation monitoring system
|
|
* @param domain Domain to add
|
|
*/
|
|
addDomainToMonitoring(domain) {
|
|
this.senderReputationMonitor.addDomain(domain);
|
|
}
|
|
/**
|
|
* Remove a domain from the reputation monitoring system
|
|
* @param domain Domain to remove
|
|
*/
|
|
removeDomainFromMonitoring(domain) {
|
|
this.senderReputationMonitor.removeDomain(domain);
|
|
}
|
|
/**
|
|
* Record an email event for domain reputation tracking
|
|
* @param domain Domain sending the email
|
|
* @param event Event details
|
|
*/
|
|
recordReputationEvent(domain, event) {
|
|
this.senderReputationMonitor.recordSendEvent(domain, event);
|
|
}
|
|
/**
|
|
* Check if DKIM key exists for a domain
|
|
* @param domain Domain to check
|
|
*/
|
|
hasDkimKey(domain) {
|
|
return this.dkimKeys.has(domain);
|
|
}
|
|
/**
|
|
* Record successful email delivery
|
|
* @param domain Sending domain
|
|
*/
|
|
recordDelivery(domain) {
|
|
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
|
|
*/
|
|
recordBounce(domain, receivingDomain, bounceType, reason) {
|
|
// 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
|
|
});
|
|
}
|
|
/**
|
|
* Get the rate limiter instance
|
|
* @returns The unified rate limiter
|
|
*/
|
|
getRateLimiter() {
|
|
return this.rateLimiter;
|
|
}
|
|
}
|
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy51bmlmaWVkLmVtYWlsLnNlcnZlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3RzL21haWwvcm91dGluZy9jbGFzc2VzLnVuaWZpZWQuZW1haWwuc2VydmVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sa0JBQWtCLENBQUM7QUFDNUMsT0FBTyxLQUFLLEtBQUssTUFBTSxnQkFBZ0IsQ0FBQztBQUN4QyxPQUFPLEVBQUUsWUFBWSxFQUFFLE1BQU0sUUFBUSxDQUFDO0FBQ3RDLE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUN6QyxPQUFPLEVBQ0wsY0FBYyxFQUNkLGdCQUFnQixFQUNoQixpQkFBaUIsRUFDbEIsTUFBTSx5QkFBeUIsQ0FBQztBQUNqQyxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0sb0NBQW9DLENBQUM7QUFDakUsT0FBTyxFQUFFLG1CQUFtQixFQUFFLE1BQU0sK0NBQStDLENBQUM7QUE4QnBGLE9BQU8sRUFBRSxXQUFXLEVBQUUsTUFBTSwyQkFBMkIsQ0FBQztBQUV4RCxPQUFPLEVBQUUsS0FBSyxFQUFFLE1BQU0sMEJBQTBCLENBQUM7QUFDakQsT0FBTyxFQUFFLGNBQWMsRUFBRSxNQUFNLDhCQUE4QixDQUFDO0FBQzlELE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSwwQkFBMEIsQ0FBQztBQUN0RCxPQUFPLEVBQUUsYUFBYSxFQUFFLFVBQVUsRUFBRSxjQUFjLEVBQUUsTUFBTSxrQ0FBa0MsQ0FBQztBQUM3RixPQUFPLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxpQ0FBaUMsQ0FBQztBQUNuRSxPQUFPLEVBQUUsc0JBQXNCLEVBQUUsTUFBTSx5Q0FBeUMsQ0FBQztBQUVqRixPQUFPLEVBQUUsdUJBQXVCLEVBQWtDLE1BQU0sd0NBQXdDLENBQUM7QUFDakgsT0FBTyxFQUFFLG9CQUFvQixFQUFzQixNQUFNLHVDQUF1QyxDQUFDO0FB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
|