This commit is contained in:
2025-05-27 14:06:22 +00:00
parent af408d38c9
commit 073c8378c7
10 changed files with 2927 additions and 746 deletions

View File

@ -12,7 +12,9 @@ export interface IEmailConfig {
// Email server settings
ports: number[];
hostname: string;
domains?: string[]; // Domains to handle email for
maxMessageSize?: number;
debug?: boolean;
// TLS configuration for email server
tls?: {
@ -47,6 +49,25 @@ export interface IEmailConfig {
maxRetryDelay?: number;
};
// Outbound email settings
outbound?: {
maxConnections?: number;
connectionTimeout?: number;
socketTimeout?: number;
retryAttempts?: number;
defaultFrom?: string;
};
// DKIM settings
dkim?: {
enabled: boolean;
selector?: string;
keySize?: number;
};
// Rate limiting configuration
rateLimits?: any; // Using any to avoid circular dependency
// Advanced MTA settings
mtaGlobalOptions?: IMtaOptions;
}

View File

@ -28,8 +28,11 @@ import * as stream from 'node:stream';
import { createSmtpServer } from '../delivery/smtpserver/index.js';
import { MultiModeDeliverySystem, type IMultiModeDeliveryOptions } from '../delivery/classes.delivery.system.js';
import { UnifiedDeliveryQueue, type IQueueOptions } from '../delivery/classes.delivery.queue.js';
import { UnifiedRateLimiter, type IHierarchicalRateLimits } from '../delivery/classes.unified.rate.limiter.js';
import { SmtpState } from '../delivery/interfaces.js';
import type { EmailProcessingMode, ISmtpSession as IBaseSmtpSession } from '../delivery/interfaces.js';
import { smtpClientMod } from '../delivery/index.js';
import type { DcRouter } from '../../classes.dcrouter.js';
/**
* Extended SMTP session interface with domain rule information
@ -48,7 +51,9 @@ export interface IUnifiedEmailServerOptions {
// Base server options
ports: number[];
hostname: string;
domains: string[]; // Domains to handle email for
banner?: string;
debug?: boolean;
// Authentication options
auth?: {
@ -84,6 +89,25 @@ export interface IUnifiedEmailServerOptions {
defaultPort?: number;
defaultTls?: boolean;
// Outbound settings
outbound?: {
maxConnections?: number;
connectionTimeout?: number;
socketTimeout?: number;
retryAttempts?: number;
defaultFrom?: string;
};
// DKIM settings
dkim?: {
enabled: boolean;
selector?: string;
keySize?: number;
};
// Rate limiting
rateLimits?: IHierarchicalRateLimits;
// Deliverability options
ipWarmupConfig?: IIPWarmupConfig;
reputationMonitorConfig?: IReputationMonitorConfig;
@ -139,6 +163,7 @@ export interface IServerStats {
* Unified email server that handles all email traffic with pattern-based routing
*/
export class UnifiedEmailServer extends EventEmitter {
private dcRouter: DcRouter;
private options: IUnifiedEmailServerOptions;
private domainRouter: DomainRouter;
private servers: any[] = [];
@ -153,9 +178,12 @@ export class UnifiedEmailServer extends EventEmitter {
private senderReputationMonitor: SenderReputationMonitor;
public deliveryQueue: UnifiedDeliveryQueue;
public deliverySystem: MultiModeDeliverySystem;
private rateLimiter: UnifiedRateLimiter;
private dkimKeys: Map<string, string> = new Map(); // domain -> private key
constructor(options: IUnifiedEmailServerOptions) {
constructor(dcRouter: DcRouter, options: IUnifiedEmailServerOptions) {
super();
this.dcRouter = dcRouter;
// Set default options
this.options = {
@ -208,6 +236,18 @@ export class UnifiedEmailServer extends EventEmitter {
cacheSize: 1000
});
// Initialize rate limiter
this.rateLimiter = new UnifiedRateLimiter(options.rateLimits || {
global: {
maxConnectionsPerIP: 10,
maxMessagesPerMinute: 100,
maxRecipientsPerMessage: 50,
maxErrorsPerIP: 10,
maxAuthFailuresPerIP: 5,
blockDuration: 300000 // 5 minutes
}
});
// Initialize delivery components
const queueOptions: IQueueOptions = {
storageType: 'memory', // Default to memory storage
@ -239,7 +279,7 @@ export class UnifiedEmailServer extends EventEmitter {
}
};
this.deliverySystem = new MultiModeDeliverySystem(this.deliveryQueue, deliveryOptions);
this.deliverySystem = new MultiModeDeliverySystem(this.deliveryQueue, deliveryOptions, this);
// Initialize statistics
this.stats = {
@ -278,6 +318,12 @@ export class UnifiedEmailServer extends EventEmitter {
await this.deliverySystem.start();
logger.log('info', 'Email delivery system started');
// Set up automatic DKIM if DNS server is available
if (this.dcRouter.dnsServer && this.options.dkim?.enabled) {
await this.setupAutomaticDkim();
logger.log('info', 'Automatic DKIM configuration completed');
}
// Ensure we have the necessary TLS options
const hasTlsConfig = this.options.tls?.keyPath && this.options.tls?.certPath;
@ -431,232 +477,8 @@ export class UnifiedEmailServer extends EventEmitter {
}
}
/**
* Handle new SMTP connection with IP reputation checking
*/
private async onConnect(session: IExtendedSmtpSession, 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: IExtendedSmtpSession, 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: IExtendedSmtpSession, 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: IExtendedSmtpSession, 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)
@ -1145,6 +967,143 @@ export class UnifiedEmailServer extends EventEmitter {
this.stats.connections.current = 0;
}
/**
* Set up automatic DKIM configuration with DNS server
*/
private async setupAutomaticDkim(): Promise<void> {
if (!this.options.domains || this.options.domains.length === 0) {
logger.log('warn', 'No domains configured for DKIM');
return;
}
const selector = this.options.dkim?.selector || 'default';
const keySize = this.options.dkim?.keySize || 2048;
for (const domain of this.options.domains) {
try {
// Check if DKIM keys already exist for this domain
let keyPair: { privateKey: string; publicKey: string };
try {
// Try to read existing keys
keyPair = await this.dkimCreator.readDKIMKeys(domain);
logger.log('info', `Using existing DKIM keys for domain: ${domain}`);
} catch (error) {
// Generate new keys if they don't exist
keyPair = await this.dkimCreator.createDKIMKeys();
// Store them for future use
await this.dkimCreator.createAndStoreDKIMKeys(domain);
logger.log('info', `Generated new DKIM keys for domain: ${domain}`);
}
// Store the private key for signing
this.dkimKeys.set(domain, keyPair.privateKey);
// Extract the public key for DNS
const publicKeyBase64 = keyPair.publicKey
.replace(/-----BEGIN PUBLIC KEY-----/g, '')
.replace(/-----END PUBLIC KEY-----/g, '')
.replace(/\s/g, '');
// Register DNS handler for this domain's DKIM records
this.dcRouter.dnsServer.registerHandler(
`${selector}._domainkey.${domain}`,
['TXT'],
() => ({
name: `${selector}._domainkey.${domain}`,
type: 'TXT',
class: 'IN',
ttl: 300,
data: `v=DKIM1; k=rsa; p=${publicKeyBase64}`
})
);
logger.log('info', `DKIM DNS handler registered for domain: ${domain} with selector: ${selector}`);
} catch (error) {
logger.log('error', `Failed to set up DKIM for domain ${domain}: ${error.message}`);
}
}
}
/**
* Get SMTP client for a specific destination
*/
public getSmtpClient(host: string, port: number): smtpClientMod.SmtpClient {
const key = `${host}:${port}`;
if (!this.smtpClients.has(key)) {
// Create a new client for this destination
const client = smtpClientMod.createSmtpClient({
...this.smtpClientConfig,
host,
port
});
this.smtpClients.set(key, client);
}
return this.smtpClients.get(key)!;
}
/**
* Generate SmartProxy routes for email ports
*/
public generateProxyRoutes(portMapping?: Record<number, number>): any[] {
const routes: any[] = [];
const defaultPortMapping = {
25: 10025,
587: 10587,
465: 10465
};
const actualPortMapping = portMapping || defaultPortMapping;
// Generate routes for each configured port
for (const externalPort of this.options.ports) {
const internalPort = actualPortMapping[externalPort] || externalPort + 10000;
let routeName = 'email-route';
let tlsMode = 'passthrough';
// Configure based on port
switch (externalPort) {
case 25:
routeName = 'smtp-route';
tlsMode = 'passthrough'; // STARTTLS
break;
case 587:
routeName = 'submission-route';
tlsMode = 'passthrough'; // STARTTLS
break;
case 465:
routeName = 'smtps-route';
tlsMode = 'terminate'; // Implicit TLS
break;
default:
routeName = `email-port-${externalPort}-route`;
}
routes.push({
name: routeName,
match: {
ports: [externalPort]
},
action: {
type: 'forward',
target: {
host: 'localhost',
port: internalPort
},
tls: {
mode: tlsMode
}
}
});
}
return routes;
}
/**
* Update server configuration
*/
@ -1720,4 +1679,58 @@ export class UnifiedEmailServer extends EventEmitter {
}): void {
this.senderReputationMonitor.recordSendEvent(domain, event);
}
/**
* Check if DKIM key exists for a domain
* @param domain Domain to check
*/
public hasDkimKey(domain: string): boolean {
return this.dkimKeys.has(domain);
}
/**
* Record successful email delivery
* @param domain Sending domain
*/
public recordDelivery(domain: string): void {
this.recordReputationEvent(domain, {
type: 'delivered',
count: 1
});
}
/**
* Record email bounce
* @param domain Sending domain
* @param receivingDomain Receiving domain that bounced
* @param bounceType Type of bounce (hard/soft)
* @param reason Bounce reason
*/
public recordBounce(domain: string, receivingDomain: string, bounceType: 'hard' | 'soft', reason: string): void {
// Record bounce in bounce manager
const bounceRecord = {
id: `bounce_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
recipient: `user@${receivingDomain}`,
sender: `user@${domain}`,
domain: domain,
bounceType: bounceType === 'hard' ? BounceType.INVALID_RECIPIENT : BounceType.TEMPORARY_FAILURE,
bounceCategory: bounceType === 'hard' ? BounceCategory.HARD : BounceCategory.SOFT,
timestamp: Date.now(),
smtpResponse: reason,
diagnosticCode: reason,
statusCode: bounceType === 'hard' ? '550' : '450',
processed: false
};
// Process the bounce
this.bounceManager.processBounce(bounceRecord);
// Record reputation event
this.recordReputationEvent(domain, {
type: 'bounce',
count: 1,
hardBounce: bounceType === 'hard',
receivingDomain
});
}
}

File diff suppressed because it is too large Load Diff