feat(mailer-smtp): add in-process security pipeline for SMTP delivery (DKIM/SPF/DMARC, content scanning, IP reputation)

This commit is contained in:
2026-02-10 22:26:20 +00:00
parent 595634fb0f
commit eb2643de93
151 changed files with 477 additions and 47531 deletions

View File

@@ -46,7 +46,6 @@ 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 type { SmtpClient } from '../delivery/smtpclient/smtp-client.js';
import { MultiModeDeliverySystem, type IMultiModeDeliveryOptions } from '../delivery/classes.delivery.system.js';
@@ -391,13 +390,6 @@ export class UnifiedEmailServer extends EventEmitter {
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;
@@ -485,54 +477,6 @@ export class UnifiedEmailServer extends EventEmitter {
}
}
/**
* Handle a socket from smartproxy in socket-handler mode
* @param socket The socket to handle
* @param port The port this connection is for (25, 587, 465)
*/
public async handleSocket(socket: plugins.net.Socket | plugins.tls.TLSSocket, port: number): Promise<void> {
if (!this.options.useSocketHandler) {
logger.log('error', 'handleSocket called but useSocketHandler is not enabled');
socket.destroy();
return;
}
logger.log('info', `Handling socket for port ${port}`);
// Create a temporary SMTP server instance for this connection
// We need a full server instance because the SMTP protocol handler needs all components
const smtpServerOptions = {
port,
hostname: this.options.hostname,
key: this.options.tls?.keyPath ? plugins.fs.readFileSync(this.options.tls.keyPath, 'utf8') : undefined,
cert: this.options.tls?.certPath ? plugins.fs.readFileSync(this.options.tls.certPath, 'utf8') : undefined
};
// Create the SMTP server instance
const smtpServer = createSmtpServer(this, smtpServerOptions);
// Get the connection manager from the server
const connectionManager = (smtpServer as any).connectionManager;
if (!connectionManager) {
logger.log('error', 'Could not get connection manager from SMTP server');
socket.destroy();
return;
}
// Determine if this is a secure connection
// Port 465 uses implicit TLS, so the socket is already secure
const isSecure = port === 465 || socket instanceof plugins.tls.TLSSocket;
// Pass the socket to the connection manager
try {
await connectionManager.handleConnection(socket, isSecure);
} catch (error) {
logger.log('error', `Error handling socket connection: ${error.message}`);
socket.destroy();
}
}
/**
* Stop the unified email server
*/
@@ -639,6 +583,11 @@ export class UnifiedEmailServer extends EventEmitter {
session.user = { username: authenticatedUser };
}
// Attach pre-computed security results from Rust in-process pipeline
if (data.securityResults) {
(session as any)._precomputedSecurityResults = data.securityResults;
}
// Process the email through the routing system
await this.processEmailByMode(rawMessageBuffer, session);
@@ -691,24 +640,34 @@ export class UnifiedEmailServer extends EventEmitter {
}
/**
* Verify inbound email security (DKIM/SPF/DMARC) using the Rust bridge.
* Falls back gracefully if the bridge is not running.
* Verify inbound email security (DKIM/SPF/DMARC) using pre-computed Rust results
* or falling back to IPC call if no pre-computed results are available.
*/
private async verifyInboundSecurity(email: Email, session: IExtendedSmtpSession): Promise<void> {
try {
const rawMessage = session.emailData || email.toRFC822String();
const result = await this.rustBridge.verifyEmail({
rawMessage,
ip: session.remoteAddress,
heloDomain: session.clientHostname || '',
hostname: this.options.hostname,
mailFrom: session.envelope?.mailFrom?.address || session.mailFrom || '',
});
// Check for pre-computed results from Rust in-process security pipeline
const precomputed = (session as any)._precomputedSecurityResults;
let result: any;
if (precomputed) {
logger.log('info', 'Using pre-computed security results from Rust in-process pipeline');
result = precomputed;
} else {
// Fallback: IPC round-trip to Rust (for backward compat / handleSocket mode)
const rawMessage = session.emailData || email.toRFC822String();
result = await this.rustBridge.verifyEmail({
rawMessage,
ip: session.remoteAddress,
heloDomain: session.clientHostname || '',
hostname: this.options.hostname,
mailFrom: session.envelope?.mailFrom?.address || session.mailFrom || '',
});
}
// Apply DKIM result headers
if (result.dkim && result.dkim.length > 0) {
const dkimSummary = result.dkim
.map(d => `${d.status}${d.domain ? ` (${d.domain})` : ''}`)
.map((d: any) => `${d.status}${d.domain ? ` (${d.domain})` : ''}`)
.join(', ');
email.addHeader('X-DKIM-Result', dkimSummary);
}
@@ -737,6 +696,31 @@ export class UnifiedEmailServer extends EventEmitter {
}
}
// Apply content scan results (from pre-computed pipeline)
if (result.contentScan) {
const scan = result.contentScan;
if (scan.threatScore > 0) {
email.addHeader('X-Spam-Score', String(scan.threatScore));
if (scan.threatType) {
email.addHeader('X-Spam-Type', scan.threatType);
}
if (scan.threatScore >= 50) {
email.mightBeSpam = true;
logger.log('warn', `Content scan threat score ${scan.threatScore} (${scan.threatType}) — marking as potential spam`);
}
}
}
// Apply IP reputation results (from pre-computed pipeline)
if (result.ipReputation) {
const rep = result.ipReputation;
email.addHeader('X-IP-Reputation-Score', String(rep.score));
if (rep.is_spam) {
email.mightBeSpam = true;
logger.log('warn', `IP ${rep.ip} flagged by reputation check (score=${rep.score}) — marking as potential spam`);
}
}
logger.log('info', `Inbound security verified for email from ${session.remoteAddress}: DKIM=${result.dkim?.[0]?.status ?? 'none'}, SPF=${result.spf?.result ?? 'none'}, DMARC=${result.dmarc?.action ?? 'none'}`);
} catch (err) {
logger.log('warn', `Inbound security verification failed: ${(err as Error).message} — accepting email`);