update
This commit is contained in:
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
1723
ts/mail/routing/classes.unified.email.server.ts.backup
Normal file
1723
ts/mail/routing/classes.unified.email.server.ts.backup
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user