feat(mailer-smtp): add in-process security pipeline for SMTP delivery (DKIM/SPF/DMARC, content scanning, IP reputation)
This commit is contained in:
@@ -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`);
|
||||
|
||||
Reference in New Issue
Block a user