feat(mailer-smtp): implement in-process SMTP server and management IPC integration

This commit is contained in:
2026-02-10 22:00:44 +00:00
parent fc2e6d44f4
commit 5220ee0857
24 changed files with 4390 additions and 274 deletions

View File

@@ -10,6 +10,7 @@ import {
import { DKIMCreator } from '../security/classes.dkimcreator.js';
import { IPReputationChecker } from '../../security/classes.ipreputationchecker.js';
import { RustSecurityBridge } from '../../security/classes.rustsecuritybridge.js';
import type { IEmailReceivedEvent, IAuthRequestEvent, IEmailData } from '../../security/classes.rustsecuritybridge.js';
// Deliverability types (IPWarmupManager and SenderReputationMonitor are optional external modules)
interface IIPWarmupConfig {
enabled?: boolean;
@@ -396,134 +397,86 @@ export class UnifiedEmailServer extends EventEmitter {
this.emit('started');
return;
}
// 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;
let tlsCertPem: string | undefined;
let tlsKeyPem: string | undefined;
if (hasTlsConfig) {
try {
key = plugins.fs.readFileSync(this.options.tls.keyPath!, 'utf8');
cert = plugins.fs.readFileSync(this.options.tls.certPath!, 'utf8');
tlsKeyPem = plugins.fs.readFileSync(this.options.tls.keyPath!, 'utf8');
tlsCertPem = 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
}
},
// Security verification delegated to the Rust bridge
dkimVerifier: {
verify: async (rawMessage: string) => {
try {
const results = await this.rustBridge.verifyDkim(rawMessage);
const first = results[0];
return { isValid: first?.is_valid ?? false, domain: first?.domain ?? '' };
} catch (err) {
logger.log('warn', `Rust DKIM verification failed: ${(err as Error).message}`);
return { isValid: false, domain: '' };
}
}
},
spfVerifier: {
verifyAndApply: async (session: any) => {
if (!session?.remoteAddress || session.remoteAddress === '127.0.0.1') {
return true; // localhost — skip SPF
}
try {
const result = await this.rustBridge.checkSpf({
ip: session.remoteAddress,
heloDomain: session.clientHostname || '',
hostname: this.options.hostname,
mailFrom: session.envelope?.mailFrom?.address || session.mailFrom || '',
});
return result.result === 'pass' || result.result === 'none' || result.result === 'neutral';
} catch (err) {
logger.log('warn', `Rust SPF check failed: ${(err as Error).message}`);
return true; // Accept on error to avoid blocking mail
}
}
},
dmarcVerifier: {
verify: async () => ({}),
applyPolicy: () => true
},
processIncomingEmail: async (email: Email) => {
// Process email using the new route-based system
await this.processEmailByMode(email, {
id: 'session-' + Math.random().toString(36).substring(2),
state: SmtpState.FINISHED,
mailFrom: email.from,
rcptTo: email.to,
emailData: email.toRFC822String(), // Use the proper method to get the full email content
useTLS: false,
connectionEnded: true,
remoteAddress: '127.0.0.1',
clientHostname: '',
secure: false,
authenticated: false,
envelope: {
mailFrom: { address: email.from, args: {} },
rcptTo: email.to.map(recipient => ({ address: recipient, args: {} }))
}
});
return true;
}
};
// Create server options
const serverOptions = {
port,
hostname: this.options.hostname,
key,
cert
};
// Create and start the SMTP server
const smtpServer = createSmtpServer(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}`);
// Event handlers are managed internally by the SmtpServer class
// No need to access the private server property
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);
}
}
});
// --- Start Rust SMTP server ---
// Register event handlers for email reception and auth
this.rustBridge.onEmailReceived(async (data) => {
try {
await this.handleRustEmailReceived(data);
} catch (err) {
logger.log('error', `Error handling email from Rust SMTP: ${(err as Error).message}`);
// Send rejection back to Rust
await this.rustBridge.sendEmailProcessingResult({
correlationId: data.correlationId,
accepted: false,
smtpCode: 451,
smtpMessage: 'Internal processing error',
});
}
});
this.rustBridge.onAuthRequest(async (data) => {
try {
await this.handleRustAuthRequest(data);
} catch (err) {
logger.log('error', `Error handling auth from Rust SMTP: ${(err as Error).message}`);
await this.rustBridge.sendAuthResult({
correlationId: data.correlationId,
success: false,
message: 'Internal auth error',
});
}
});
// Determine which ports need STARTTLS and which need implicit TLS
const smtpPorts = (this.options.ports as number[]).filter(p => p !== 465);
const securePort = (this.options.ports as number[]).find(p => p === 465);
const started = await this.rustBridge.startSmtpServer({
hostname: this.options.hostname,
ports: smtpPorts,
securePort: securePort,
tlsCertPem,
tlsKeyPem,
maxMessageSize: this.options.maxMessageSize || 10 * 1024 * 1024,
maxConnections: this.options.maxConnections || this.options.maxClients || 100,
maxRecipients: 100,
connectionTimeoutSecs: this.options.connectionTimeout ? Math.floor(this.options.connectionTimeout / 1000) : 30,
dataTimeoutSecs: 60,
authEnabled: !!this.options.auth?.required || !!(this.options.auth?.users?.length),
maxAuthFailures: 3,
socketTimeoutSecs: this.options.socketTimeout ? Math.floor(this.options.socketTimeout / 1000) : 300,
processingTimeoutSecs: 30,
rateLimits: this.options.rateLimits ? {
maxConnectionsPerIp: this.options.rateLimits.global?.maxConnectionsPerIP || 50,
maxMessagesPerSender: this.options.rateLimits.global?.maxMessagesPerMinute || 100,
maxAuthFailuresPerIp: this.options.rateLimits.global?.maxAuthFailuresPerIP || 5,
windowSecs: 60,
} : undefined,
});
if (!started) {
throw new Error('Failed to start Rust SMTP server');
}
logger.log('info', `Rust SMTP server listening on ports: ${smtpPorts.join(', ')}${securePort ? ` + ${securePort} (TLS)` : ''}`);
logger.log('info', 'UnifiedEmailServer started successfully');
this.emit('started');
} catch (error) {
@@ -587,6 +540,14 @@ export class UnifiedEmailServer extends EventEmitter {
logger.log('info', 'Stopping UnifiedEmailServer');
try {
// Stop the Rust SMTP server first
try {
await this.rustBridge.stopSmtpServer();
logger.log('info', 'Rust SMTP server stopped');
} catch (err) {
logger.log('warn', `Error stopping Rust SMTP server: ${(err as Error).message}`);
}
// Clear the servers array - servers will be garbage collected
this.servers = [];
@@ -623,11 +584,112 @@ export class UnifiedEmailServer extends EventEmitter {
throw error;
}
}
// -----------------------------------------------------------------------
// Rust SMTP server event handlers
// -----------------------------------------------------------------------
/**
* Handle an emailReceived event from the Rust SMTP server.
* Decodes the email data, processes it through the routing system,
* and sends back the result via the correlation-ID callback.
*/
private async handleRustEmailReceived(data: IEmailReceivedEvent): Promise<void> {
const { correlationId, mailFrom, rcptTo, remoteAddr, clientHostname, secure, authenticatedUser } = data;
logger.log('info', `Rust SMTP received email from=${mailFrom} to=${rcptTo.join(',')} remote=${remoteAddr}`);
try {
// Decode the email data
let rawMessageBuffer: Buffer;
if (data.data.type === 'inline' && data.data.base64) {
rawMessageBuffer = Buffer.from(data.data.base64, 'base64');
} else if (data.data.type === 'file' && data.data.path) {
rawMessageBuffer = plugins.fs.readFileSync(data.data.path);
// Clean up temp file
try {
plugins.fs.unlinkSync(data.data.path);
} catch {
// Ignore cleanup errors
}
} else {
throw new Error('Invalid email data transport');
}
// Build a session-like object for processEmailByMode
const session: IExtendedSmtpSession = {
id: data.sessionId || 'rust-' + Math.random().toString(36).substring(2),
state: SmtpState.FINISHED,
mailFrom: mailFrom,
rcptTo: rcptTo,
emailData: rawMessageBuffer.toString('utf8'),
useTLS: secure,
connectionEnded: false,
remoteAddress: remoteAddr,
clientHostname: clientHostname || '',
secure: secure,
authenticated: !!authenticatedUser,
envelope: {
mailFrom: { address: mailFrom, args: {} },
rcptTo: rcptTo.map(addr => ({ address: addr, args: {} })),
},
};
if (authenticatedUser) {
session.user = { username: authenticatedUser };
}
// Process the email through the routing system
await this.processEmailByMode(rawMessageBuffer, session);
// Send acceptance back to Rust
await this.rustBridge.sendEmailProcessingResult({
correlationId,
accepted: true,
smtpCode: 250,
smtpMessage: '2.0.0 Message accepted for delivery',
});
} catch (err) {
logger.log('error', `Failed to process email from Rust SMTP: ${(err as Error).message}`);
await this.rustBridge.sendEmailProcessingResult({
correlationId,
accepted: false,
smtpCode: 550,
smtpMessage: `5.0.0 Processing failed: ${(err as Error).message}`,
});
}
}
/**
* Handle an authRequest event from the Rust SMTP server.
* Validates credentials and sends back the result.
*/
private async handleRustAuthRequest(data: IAuthRequestEvent): Promise<void> {
const { correlationId, username, password, remoteAddr } = data;
logger.log('info', `Rust SMTP auth request for user=${username} from=${remoteAddr}`);
// Check against configured users
const users = this.options.auth?.users || [];
const matched = users.find(
u => u.username === username && u.password === password
);
if (matched) {
await this.rustBridge.sendAuthResult({
correlationId,
success: true,
});
} else {
logger.log('warn', `Auth failed for user=${username} from=${remoteAddr}`);
await this.rustBridge.sendAuthResult({
correlationId,
success: false,
message: 'Invalid credentials',
});
}
}
/**
* Verify inbound email security (DKIM/SPF/DMARC) using the Rust bridge.
* Falls back gracefully if the bridge is not running.