1720 lines
54 KiB
TypeScript
1720 lines
54 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 { DomainRouter } from './classes.domain.router.js';
|
|
import type {
|
|
IEmailConfig,
|
|
EmailProcessingMode,
|
|
IDomainRule
|
|
} from './classes.email.config.js';
|
|
import { Email } from '../core/classes.email.js';
|
|
import { BounceManager, BounceType, BounceCategory } from '../core/classes.bouncemanager.js';
|
|
import * as net from 'node:net';
|
|
import * as tls from 'node:tls';
|
|
import * as stream from 'node:stream';
|
|
import { SMTPServer as MtaSmtpServer } from '../delivery/classes.smtpserver.js';
|
|
import { MultiModeDeliverySystem, type IMultiModeDeliveryOptions } from '../delivery/classes.delivery.system.js';
|
|
import { UnifiedDeliveryQueue, type IQueueOptions } from '../delivery/classes.delivery.queue.js';
|
|
|
|
/**
|
|
* Options for the unified email server
|
|
*/
|
|
export interface IUnifiedEmailServerOptions {
|
|
// Base server options
|
|
ports: number[];
|
|
hostname: string;
|
|
banner?: string;
|
|
|
|
// 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;
|
|
|
|
// Domain rules
|
|
domainRules: IDomainRule[];
|
|
|
|
// Default handling for unmatched domains
|
|
defaultMode: EmailProcessingMode;
|
|
defaultServer?: string;
|
|
defaultPort?: number;
|
|
defaultTls?: boolean;
|
|
|
|
// Deliverability options
|
|
ipWarmupConfig?: IIPWarmupConfig;
|
|
reputationMonitorConfig?: IReputationMonitorConfig;
|
|
}
|
|
|
|
/**
|
|
* Interface describing SMTP session data
|
|
*/
|
|
export interface ISmtpSession {
|
|
id: string;
|
|
remoteAddress: string;
|
|
clientHostname: string;
|
|
secure: boolean;
|
|
authenticated: boolean;
|
|
user?: {
|
|
username: string;
|
|
[key: string]: any;
|
|
};
|
|
envelope: {
|
|
mailFrom: {
|
|
address: string;
|
|
args: any;
|
|
};
|
|
rcptTo: Array<{
|
|
address: string;
|
|
args: any;
|
|
}>;
|
|
};
|
|
processingMode?: EmailProcessingMode;
|
|
matchedRule?: IDomainRule;
|
|
}
|
|
|
|
/**
|
|
* Authentication data for SMTP
|
|
*/
|
|
export interface IAuthData {
|
|
method: string;
|
|
username: string;
|
|
password: string;
|
|
}
|
|
|
|
/**
|
|
* 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 options: IUnifiedEmailServerOptions;
|
|
private domainRouter: DomainRouter;
|
|
private servers: MtaSmtpServer[] = [];
|
|
private stats: IServerStats;
|
|
private processingTimes: number[] = [];
|
|
|
|
// Add components needed for sending and securing emails
|
|
public dkimCreator: DKIMCreator;
|
|
private ipReputationChecker: IPReputationChecker;
|
|
private bounceManager: BounceManager;
|
|
private ipWarmupManager: IPWarmupManager;
|
|
private senderReputationMonitor: SenderReputationMonitor;
|
|
public deliveryQueue: UnifiedDeliveryQueue;
|
|
public deliverySystem: MultiModeDeliverySystem;
|
|
|
|
constructor(options: IUnifiedEmailServerOptions) {
|
|
super();
|
|
|
|
// 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 domain router for pattern matching
|
|
this.domainRouter = new DomainRouter({
|
|
domainRules: options.domainRules,
|
|
defaultMode: options.defaultMode,
|
|
defaultServer: options.defaultServer,
|
|
defaultPort: options.defaultPort,
|
|
defaultTls: options.defaultTls,
|
|
enableCache: true,
|
|
cacheSize: 1000
|
|
});
|
|
|
|
// 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);
|
|
|
|
// 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
|
|
}
|
|
|
|
/**
|
|
* 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');
|
|
|
|
// 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) => {
|
|
// This is where we'll process the email based on domain routing
|
|
const to = email.to[0]; // Email.to is an array, take the first recipient
|
|
const rule = this.domainRouter.matchRule(to);
|
|
const mode = rule?.mode || this.options.defaultMode;
|
|
|
|
// Process based on the mode
|
|
await this.processEmailByMode(email, {
|
|
id: 'session-' + Math.random().toString(36).substring(2),
|
|
remoteAddress: '127.0.0.1',
|
|
clientHostname: '',
|
|
secure: false,
|
|
authenticated: false,
|
|
envelope: {
|
|
mailFrom: { address: email.from, args: {} },
|
|
rcptTo: email.to.map(recipient => ({ address: recipient, args: {} }))
|
|
},
|
|
processingMode: mode,
|
|
matchedRule: rule
|
|
}, mode);
|
|
|
|
return true;
|
|
}
|
|
};
|
|
|
|
// Create server options
|
|
const serverOptions = {
|
|
port,
|
|
hostname: this.options.hostname,
|
|
key,
|
|
cert
|
|
};
|
|
|
|
// Create and start the SMTP server
|
|
const smtpServer = new MtaSmtpServer(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}`);
|
|
|
|
// Set up event handlers
|
|
(smtpServer as any).server.on('error', (err: Error) => {
|
|
logger.log('error', `SMTP server error on port ${port}: ${err.message}`);
|
|
this.emit('error', err);
|
|
});
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stop the unified email server
|
|
*/
|
|
public async stop(): Promise<void> {
|
|
logger.log('info', 'Stopping UnifiedEmailServer');
|
|
|
|
try {
|
|
// Stop all SMTP servers
|
|
for (const server of this.servers) {
|
|
// Nothing to do, servers will be garbage collected
|
|
// The server.stop() method is not needed during this transition
|
|
}
|
|
|
|
// Clear the servers array
|
|
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');
|
|
}
|
|
|
|
logger.log('info', 'UnifiedEmailServer stopped successfully');
|
|
this.emit('stopped');
|
|
} catch (error) {
|
|
logger.log('error', `Error stopping UnifiedEmailServer: ${error.message}`);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle new SMTP connection with IP reputation checking
|
|
*/
|
|
private async onConnect(session: ISmtpSession, callback: (err?: Error) => void): Promise<void> {
|
|
logger.log('info', `New connection from ${session.remoteAddress}`);
|
|
|
|
// Update connection statistics
|
|
this.stats.connections.current++;
|
|
this.stats.connections.total++;
|
|
|
|
// Log connection event
|
|
SecurityLogger.getInstance().logEvent({
|
|
level: SecurityLogLevel.INFO,
|
|
type: SecurityEventType.CONNECTION,
|
|
message: 'New SMTP connection established',
|
|
ipAddress: session.remoteAddress,
|
|
details: {
|
|
sessionId: session.id,
|
|
secure: session.secure
|
|
}
|
|
});
|
|
|
|
// Perform IP reputation check
|
|
try {
|
|
const ipReputation = await this.ipReputationChecker.checkReputation(session.remoteAddress);
|
|
|
|
// Store reputation in session for later use
|
|
(session as any).ipReputation = ipReputation;
|
|
|
|
logger.log('info', `IP reputation check for ${session.remoteAddress}: score=${ipReputation.score}, isSpam=${ipReputation.isSpam}`);
|
|
|
|
// Reject connection if reputation is too low and rejection is enabled
|
|
if (ipReputation.score < 20 && (this.options as any).security?.rejectHighRiskIPs) {
|
|
const error = new Error(`Connection rejected: IP ${session.remoteAddress} has poor reputation score (${ipReputation.score})`);
|
|
|
|
SecurityLogger.getInstance().logEvent({
|
|
level: SecurityLogLevel.WARN,
|
|
type: SecurityEventType.REJECTED_CONNECTION,
|
|
message: 'Connection rejected due to poor IP reputation',
|
|
ipAddress: session.remoteAddress,
|
|
details: {
|
|
sessionId: session.id,
|
|
reputationScore: ipReputation.score,
|
|
isSpam: ipReputation.isSpam,
|
|
isProxy: ipReputation.isProxy,
|
|
isTor: ipReputation.isTor,
|
|
isVPN: ipReputation.isVPN
|
|
},
|
|
success: false
|
|
});
|
|
|
|
return callback(error);
|
|
}
|
|
|
|
// For suspicious IPs, add a note but allow connection
|
|
if (ipReputation.score < 50) {
|
|
logger.log('warn', `Suspicious IP connection allowed: ${session.remoteAddress} (score: ${ipReputation.score})`);
|
|
}
|
|
} catch (error) {
|
|
// Log error but continue with connection
|
|
logger.log('error', `Error checking IP reputation for ${session.remoteAddress}: ${error.message}`);
|
|
}
|
|
|
|
// Continue with the connection
|
|
callback();
|
|
}
|
|
|
|
/**
|
|
* Handle authentication (stub implementation)
|
|
*/
|
|
private onAuth(auth: IAuthData, session: ISmtpSession, callback: (err?: Error, user?: any) => void): void {
|
|
if (!this.options.auth || !this.options.auth.users || this.options.auth.users.length === 0) {
|
|
// No authentication configured, reject
|
|
const error = new Error('Authentication not supported');
|
|
logger.log('warn', `Authentication attempt when not configured: ${auth.username}`);
|
|
|
|
SecurityLogger.getInstance().logEvent({
|
|
level: SecurityLogLevel.WARN,
|
|
type: SecurityEventType.AUTHENTICATION,
|
|
message: 'Authentication attempt when not configured',
|
|
ipAddress: session.remoteAddress,
|
|
details: {
|
|
username: auth.username,
|
|
method: auth.method,
|
|
sessionId: session.id
|
|
},
|
|
success: false
|
|
});
|
|
|
|
return callback(error);
|
|
}
|
|
|
|
// Find matching user
|
|
const user = this.options.auth.users.find(u => u.username === auth.username && u.password === auth.password);
|
|
|
|
if (user) {
|
|
logger.log('info', `User ${auth.username} authenticated successfully`);
|
|
|
|
SecurityLogger.getInstance().logEvent({
|
|
level: SecurityLogLevel.INFO,
|
|
type: SecurityEventType.AUTHENTICATION,
|
|
message: 'SMTP authentication successful',
|
|
ipAddress: session.remoteAddress,
|
|
details: {
|
|
username: auth.username,
|
|
method: auth.method,
|
|
sessionId: session.id
|
|
},
|
|
success: true
|
|
});
|
|
|
|
return callback(null, { username: user.username });
|
|
} else {
|
|
const error = new Error('Invalid username or password');
|
|
logger.log('warn', `Failed authentication for ${auth.username}`);
|
|
|
|
SecurityLogger.getInstance().logEvent({
|
|
level: SecurityLogLevel.WARN,
|
|
type: SecurityEventType.AUTHENTICATION,
|
|
message: 'SMTP authentication failed',
|
|
ipAddress: session.remoteAddress,
|
|
details: {
|
|
username: auth.username,
|
|
method: auth.method,
|
|
sessionId: session.id
|
|
},
|
|
success: false
|
|
});
|
|
|
|
return callback(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle MAIL FROM command (stub implementation)
|
|
*/
|
|
private onMailFrom(address: {address: string}, session: ISmtpSession, callback: (err?: Error) => void): void {
|
|
logger.log('info', `MAIL FROM: ${address.address}`);
|
|
|
|
// Validate the email address
|
|
if (!this.isValidEmail(address.address)) {
|
|
const error = new Error('Invalid sender address');
|
|
logger.log('warn', `Invalid sender address: ${address.address}`);
|
|
|
|
SecurityLogger.getInstance().logEvent({
|
|
level: SecurityLogLevel.WARN,
|
|
type: SecurityEventType.EMAIL_VALIDATION,
|
|
message: 'Invalid sender email format',
|
|
ipAddress: session.remoteAddress,
|
|
details: {
|
|
address: address.address,
|
|
sessionId: session.id
|
|
},
|
|
success: false
|
|
});
|
|
|
|
return callback(error);
|
|
}
|
|
|
|
// Authentication check if required
|
|
if (this.options.auth?.required && !session.authenticated) {
|
|
const error = new Error('Authentication required');
|
|
logger.log('warn', `Unauthenticated sender rejected: ${address.address}`);
|
|
|
|
SecurityLogger.getInstance().logEvent({
|
|
level: SecurityLogLevel.WARN,
|
|
type: SecurityEventType.AUTHENTICATION,
|
|
message: 'Unauthenticated sender rejected',
|
|
ipAddress: session.remoteAddress,
|
|
details: {
|
|
address: address.address,
|
|
sessionId: session.id
|
|
},
|
|
success: false
|
|
});
|
|
|
|
return callback(error);
|
|
}
|
|
|
|
// Continue processing
|
|
callback();
|
|
}
|
|
|
|
/**
|
|
* Handle RCPT TO command (stub implementation)
|
|
*/
|
|
private onRcptTo(address: {address: string}, session: ISmtpSession, callback: (err?: Error) => void): void {
|
|
logger.log('info', `RCPT TO: ${address.address}`);
|
|
|
|
// Validate the email address
|
|
if (!this.isValidEmail(address.address)) {
|
|
const error = new Error('Invalid recipient address');
|
|
logger.log('warn', `Invalid recipient address: ${address.address}`);
|
|
|
|
SecurityLogger.getInstance().logEvent({
|
|
level: SecurityLogLevel.WARN,
|
|
type: SecurityEventType.EMAIL_VALIDATION,
|
|
message: 'Invalid recipient email format',
|
|
ipAddress: session.remoteAddress,
|
|
details: {
|
|
address: address.address,
|
|
sessionId: session.id
|
|
},
|
|
success: false
|
|
});
|
|
|
|
return callback(error);
|
|
}
|
|
|
|
// Pattern match the recipient to determine processing mode
|
|
const rule = this.domainRouter.matchRule(address.address);
|
|
|
|
if (rule) {
|
|
// Store the matched rule and processing mode in the session
|
|
session.matchedRule = rule;
|
|
session.processingMode = rule.mode;
|
|
logger.log('info', `Email ${address.address} matched rule: ${rule.pattern}, mode: ${rule.mode}`);
|
|
} else {
|
|
// Use default mode
|
|
session.processingMode = this.options.defaultMode;
|
|
logger.log('info', `Email ${address.address} using default mode: ${this.options.defaultMode}`);
|
|
}
|
|
|
|
// Continue processing
|
|
callback();
|
|
}
|
|
|
|
/**
|
|
* Handle incoming email data (stub implementation)
|
|
*/
|
|
private onData(stream: stream.Readable, session: ISmtpSession, callback: (err?: Error) => void): void {
|
|
logger.log('info', `Processing email data for session ${session.id}`);
|
|
|
|
const startTime = Date.now();
|
|
const chunks: Buffer[] = [];
|
|
|
|
stream.on('data', (chunk: Buffer) => {
|
|
chunks.push(chunk);
|
|
});
|
|
|
|
stream.on('end', async () => {
|
|
try {
|
|
const data = Buffer.concat(chunks);
|
|
const mode = session.processingMode || this.options.defaultMode;
|
|
|
|
// Determine processing mode based on matched rule
|
|
const processedEmail = await this.processEmailByMode(data, session, mode);
|
|
|
|
// Update statistics
|
|
this.stats.messages.processed++;
|
|
this.stats.messages.delivered++;
|
|
|
|
// Calculate processing time
|
|
const processingTime = Date.now() - startTime;
|
|
this.processingTimes.push(processingTime);
|
|
this.updateProcessingTimeStats();
|
|
|
|
// Emit event for delivery queue
|
|
this.emit('emailProcessed', processedEmail, mode, session.matchedRule);
|
|
|
|
logger.log('info', `Email processed successfully in ${processingTime}ms, mode: ${mode}`);
|
|
callback();
|
|
} catch (error) {
|
|
logger.log('error', `Error processing email: ${error.message}`);
|
|
|
|
// Update statistics
|
|
this.stats.messages.processed++;
|
|
this.stats.messages.failed++;
|
|
|
|
// Calculate processing time for failed attempts too
|
|
const processingTime = Date.now() - startTime;
|
|
this.processingTimes.push(processingTime);
|
|
this.updateProcessingTimeStats();
|
|
|
|
SecurityLogger.getInstance().logEvent({
|
|
level: SecurityLogLevel.ERROR,
|
|
type: SecurityEventType.EMAIL_PROCESSING,
|
|
message: 'Email processing failed',
|
|
ipAddress: session.remoteAddress,
|
|
details: {
|
|
error: error.message,
|
|
sessionId: session.id,
|
|
mode: session.processingMode,
|
|
processingTime
|
|
},
|
|
success: false
|
|
});
|
|
|
|
callback(error);
|
|
}
|
|
});
|
|
|
|
stream.on('error', (err) => {
|
|
logger.log('error', `Stream error: ${err.message}`);
|
|
|
|
SecurityLogger.getInstance().logEvent({
|
|
level: SecurityLogLevel.ERROR,
|
|
type: SecurityEventType.EMAIL_PROCESSING,
|
|
message: 'Email stream error',
|
|
ipAddress: session.remoteAddress,
|
|
details: {
|
|
error: err.message,
|
|
sessionId: session.id
|
|
},
|
|
success: false
|
|
});
|
|
|
|
callback(err);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update processing time statistics
|
|
*/
|
|
private updateProcessingTimeStats(): void {
|
|
if (this.processingTimes.length === 0) return;
|
|
|
|
// Keep only the last 1000 processing times
|
|
if (this.processingTimes.length > 1000) {
|
|
this.processingTimes = this.processingTimes.slice(-1000);
|
|
}
|
|
|
|
// Calculate stats
|
|
const sum = this.processingTimes.reduce((acc, time) => acc + time, 0);
|
|
const avg = sum / this.processingTimes.length;
|
|
const max = Math.max(...this.processingTimes);
|
|
const min = Math.min(...this.processingTimes);
|
|
|
|
this.stats.processingTime = { avg, max, min };
|
|
}
|
|
|
|
/**
|
|
* Process email based on the determined mode
|
|
*/
|
|
public async processEmailByMode(emailData: Email | Buffer, session: ISmtpSession, mode: EmailProcessingMode): 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');
|
|
}
|
|
|
|
// Process based on mode
|
|
switch (mode) {
|
|
case 'forward':
|
|
await this.handleForwardMode(email, session);
|
|
break;
|
|
|
|
case 'mta':
|
|
await this.handleMtaMode(email, session);
|
|
break;
|
|
|
|
case 'process':
|
|
await this.handleProcessMode(email, session);
|
|
break;
|
|
|
|
default:
|
|
throw new Error(`Unknown processing mode: ${mode}`);
|
|
}
|
|
|
|
// Return the processed email
|
|
return email;
|
|
}
|
|
|
|
/**
|
|
* Handle email in forward mode (SMTP proxy)
|
|
*/
|
|
private async handleForwardMode(email: Email, session: ISmtpSession): Promise<void> {
|
|
logger.log('info', `Handling email in forward mode for session ${session.id}`);
|
|
|
|
// Get target server information
|
|
const rule = session.matchedRule;
|
|
const targetServer = rule?.target?.server || this.options.defaultServer;
|
|
const targetPort = rule?.target?.port || this.options.defaultPort || 25;
|
|
const useTls = rule?.target?.useTls ?? this.options.defaultTls ?? false;
|
|
|
|
if (!targetServer) {
|
|
throw new Error('No target server configured for forward mode');
|
|
}
|
|
|
|
logger.log('info', `Forwarding email to ${targetServer}:${targetPort}, TLS: ${useTls}`);
|
|
|
|
try {
|
|
// Create a simple SMTP client connection to the target server
|
|
const client = new net.Socket();
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
// Connect to the target server
|
|
client.connect({
|
|
host: targetServer,
|
|
port: targetPort
|
|
});
|
|
|
|
client.on('data', (data) => {
|
|
const response = data.toString().trim();
|
|
logger.log('debug', `SMTP response: ${response}`);
|
|
|
|
// Handle SMTP response codes
|
|
if (response.startsWith('2')) {
|
|
// Success response
|
|
resolve();
|
|
} else if (response.startsWith('5')) {
|
|
// Permanent error
|
|
reject(new Error(`SMTP error: ${response}`));
|
|
}
|
|
});
|
|
|
|
client.on('error', (err) => {
|
|
logger.log('error', `SMTP client error: ${err.message}`);
|
|
reject(err);
|
|
});
|
|
|
|
// SMTP client commands would go here in a full implementation
|
|
// For now, just finish the connection
|
|
client.end();
|
|
resolve();
|
|
});
|
|
|
|
logger.log('info', `Email forwarded successfully to ${targetServer}:${targetPort}`);
|
|
|
|
SecurityLogger.getInstance().logEvent({
|
|
level: SecurityLogLevel.INFO,
|
|
type: SecurityEventType.EMAIL_FORWARDING,
|
|
message: 'Email forwarded',
|
|
ipAddress: session.remoteAddress,
|
|
details: {
|
|
sessionId: session.id,
|
|
targetServer,
|
|
targetPort,
|
|
useTls,
|
|
ruleName: rule?.pattern || 'default',
|
|
subject: email.subject
|
|
},
|
|
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: session.remoteAddress,
|
|
details: {
|
|
sessionId: session.id,
|
|
targetServer,
|
|
targetPort,
|
|
useTls,
|
|
ruleName: rule?.pattern || 'default',
|
|
error: error.message
|
|
},
|
|
success: false
|
|
});
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle email in MTA mode (programmatic processing)
|
|
*/
|
|
private async handleMtaMode(email: Email, session: ISmtpSession): Promise<void> {
|
|
logger.log('info', `Handling email in MTA mode for session ${session.id}`);
|
|
|
|
try {
|
|
// Apply MTA rule options if provided
|
|
if (session.matchedRule?.mtaOptions) {
|
|
const options = session.matchedRule.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.matchedRule?.pattern || '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.matchedRule?.pattern || 'default',
|
|
error: error.message
|
|
},
|
|
success: false
|
|
});
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle email in process mode (store-and-forward with scanning)
|
|
*/
|
|
private async handleProcessMode(email: Email, session: ISmtpSession): Promise<void> {
|
|
logger.log('info', `Handling email in process mode for session ${session.id}`);
|
|
|
|
try {
|
|
const rule = session.matchedRule;
|
|
|
|
// Apply content scanning if enabled
|
|
if (rule?.contentScanning && rule.scanners && rule.scanners.length > 0) {
|
|
logger.log('info', 'Performing content scanning');
|
|
|
|
// Apply each scanner
|
|
for (const scanner of rule.scanners) {
|
|
switch (scanner.type) {
|
|
case 'spam':
|
|
logger.log('info', 'Scanning for spam content');
|
|
// Implement spam scanning
|
|
break;
|
|
|
|
case 'virus':
|
|
logger.log('info', 'Scanning for virus content');
|
|
// Implement virus scanning
|
|
break;
|
|
|
|
case 'attachment':
|
|
logger.log('info', 'Scanning attachments');
|
|
|
|
// Check for blocked extensions
|
|
if (scanner.blockedExtensions && scanner.blockedExtensions.length > 0) {
|
|
for (const attachment of email.attachments) {
|
|
const ext = this.getFileExtension(attachment.filename);
|
|
if (scanner.blockedExtensions.includes(ext)) {
|
|
if (scanner.action === 'reject') {
|
|
throw new Error(`Blocked attachment type: ${ext}`);
|
|
} else { // tag
|
|
email.addHeader('X-Attachment-Warning', `Potentially unsafe attachment: ${attachment.filename}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply transformations if defined
|
|
if (rule?.transformations && rule.transformations.length > 0) {
|
|
logger.log('info', 'Applying email transformations');
|
|
|
|
for (const transform of rule.transformations) {
|
|
switch (transform.type) {
|
|
case 'addHeader':
|
|
if (transform.header && transform.value) {
|
|
email.addHeader(transform.header, transform.value);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
logger.log('info', `Email successfully processed in store-and-forward mode`);
|
|
|
|
SecurityLogger.getInstance().logEvent({
|
|
level: SecurityLogLevel.INFO,
|
|
type: SecurityEventType.EMAIL_PROCESSING,
|
|
message: 'Email processed and queued',
|
|
ipAddress: session.remoteAddress,
|
|
details: {
|
|
sessionId: session.id,
|
|
ruleName: rule?.pattern || 'default',
|
|
contentScanning: rule?.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.matchedRule?.pattern || 'default',
|
|
error: error.message
|
|
},
|
|
success: false
|
|
});
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get file extension from filename
|
|
*/
|
|
private getFileExtension(filename: string): string {
|
|
return filename.substring(filename.lastIndexOf('.')).toLowerCase();
|
|
}
|
|
|
|
/**
|
|
* Handle server errors
|
|
*/
|
|
private onError(err: Error): void {
|
|
logger.log('error', `Server error: ${err.message}`);
|
|
this.emit('error', err);
|
|
}
|
|
|
|
/**
|
|
* Handle server close
|
|
*/
|
|
private onClose(): void {
|
|
logger.log('info', 'Server closed');
|
|
this.emit('close');
|
|
|
|
// Update statistics
|
|
this.stats.connections.current = 0;
|
|
}
|
|
|
|
/**
|
|
* 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 domain router if rules changed
|
|
if (options.domainRules) {
|
|
this.domainRouter.updateRules(options.domainRules);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update domain rules
|
|
*/
|
|
public updateDomainRules(rules: IDomainRule[]): void {
|
|
this.options.domainRules = rules;
|
|
this.domainRouter.updateRules(rules);
|
|
}
|
|
|
|
/**
|
|
* Get server statistics
|
|
*/
|
|
public getStats(): IServerStats {
|
|
return { ...this.stats };
|
|
}
|
|
|
|
/**
|
|
* Validate email address format
|
|
*/
|
|
private isValidEmail(email: string): boolean {
|
|
// Basic validation - a more comprehensive validation could be used
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
return emailRegex.test(email);
|
|
}
|
|
|
|
/**
|
|
* 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',
|
|
rule?: IDomainRule,
|
|
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' && rule?.mtaOptions?.dkimSign) {
|
|
const domain = email.from.split('@')[1];
|
|
await this.handleDkimSigning(email, domain, rule.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, rule);
|
|
|
|
// 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 {
|
|
// Convert Email to Smartmail format for bounce processing
|
|
const smartmailEmail = new plugins.smartmail.Smartmail({
|
|
from: bounceEmail.from,
|
|
to: [bounceEmail.to[0]], // Ensure to is an array with at least one recipient
|
|
subject: bounceEmail.subject,
|
|
body: bounceEmail.text, // Smartmail uses 'body' instead of 'text'
|
|
htmlBody: bounceEmail.html // Smartmail uses 'htmlBody' instead of 'html'
|
|
});
|
|
|
|
// Process as a bounce notification
|
|
const bounceRecord = await this.bounceManager.processBounceEmail(smartmailEmail);
|
|
|
|
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);
|
|
}
|
|
} |