BREAKING CHANGE(smtp-client): Replace the legacy TypeScript SMTP client with a new Rust-based SMTP client and IPC bridge for outbound delivery

This commit is contained in:
2026-02-11 07:17:05 +00:00
parent fc4877e06b
commit 27bab5f345
50 changed files with 2268 additions and 6737 deletions

View File

@@ -17,8 +17,7 @@ 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 { createPooledSmtpClient } from '../delivery/smtpclient/create-client.js';
import type { SmtpClient } from '../delivery/smtpclient/smtp-client.js';
import type { ISmtpSendResult, IOutboundEmail } from '../../security/classes.rustsecuritybridge.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';
@@ -167,7 +166,6 @@ export class UnifiedEmailServer extends EventEmitter {
public deliverySystem: MultiModeDeliverySystem;
private rateLimiter: UnifiedRateLimiter; // TODO: Implement rate limiting in SMTP server handlers
private dkimKeys: Map<string, string> = new Map(); // domain -> private key
private smtpClients: Map<string, SmtpClient> = new Map(); // host:port -> client
constructor(dcRouter: DcRouter, options: IUnifiedEmailServerOptions) {
super();
@@ -242,17 +240,8 @@ export class UnifiedEmailServer extends EventEmitter {
bounceHandler: {
processSmtpFailure: this.processSmtpFailure.bind(this)
},
onDeliverySuccess: async (item, _result) => {
// Record delivery success event for reputation monitoring
const email = item.processingResult as Email;
const senderDomain = email.from.split('@')[1];
if (senderDomain) {
this.recordReputationEvent(senderDomain, {
type: 'delivered',
count: email.to.length
});
}
onDeliverySuccess: async (_item, _result) => {
// Delivery success recorded via delivery system
}
};
@@ -281,34 +270,50 @@ export class UnifiedEmailServer extends EventEmitter {
}
/**
* Get or create an SMTP client for the given host and port
* Uses connection pooling for efficiency
* Send an outbound email via the Rust SMTP client.
* Uses connection pooling in the Rust binary for efficiency.
*/
public getSmtpClient(host: string, port: number = 25): SmtpClient {
const clientKey = `${host}:${port}`;
// Check if we already have a client for this destination
let client = this.smtpClients.get(clientKey);
if (!client) {
// Create a new pooled SMTP client
client = createPooledSmtpClient({
host,
port,
secure: port === 465,
connectionTimeout: this.options.outbound?.connectionTimeout || 30000,
socketTimeout: this.options.outbound?.socketTimeout || 120000,
maxConnections: this.options.outbound?.maxConnections || 10,
maxMessages: 1000, // Messages per connection before reconnect
pool: true,
debug: false
});
this.smtpClients.set(clientKey, client);
logger.log('info', `Created new SMTP client pool for ${clientKey}`);
public async sendOutboundEmail(host: string, port: number, email: Email, options?: {
auth?: { user: string; pass: string };
dkimDomain?: string;
dkimSelector?: string;
}): Promise<ISmtpSendResult> {
// Build DKIM config if domain has keys
let dkim: { domain: string; selector: string; privateKey: string } | undefined;
if (options?.dkimDomain) {
try {
const { privateKey } = await this.dkimCreator.readDKIMKeys(options.dkimDomain);
dkim = { domain: options.dkimDomain, selector: options.dkimSelector || 'default', privateKey };
} catch (err) {
logger.log('warn', `Failed to read DKIM keys for ${options.dkimDomain}: ${(err as Error).message}`);
}
}
return client;
// Serialize the Email to the outbound format
const outboundEmail: IOutboundEmail = {
from: email.from,
to: email.to,
cc: email.cc || [],
bcc: email.bcc || [],
subject: email.subject || '',
text: email.text || '',
html: email.html || undefined,
headers: email.headers as Record<string, string> || {},
};
return this.rustBridge.sendOutboundEmail({
host,
port,
secure: port === 465,
domain: this.options.hostname,
auth: options?.auth,
email: outboundEmail,
dkim,
connectionTimeoutSecs: Math.floor((this.options.outbound?.connectionTimeout || 30000) / 1000),
socketTimeoutSecs: Math.floor((this.options.outbound?.socketTimeout || 120000) / 1000),
poolKey: `${host}:${port}`,
maxPoolConnections: this.options.outbound?.maxConnections || 10,
});
}
/**
@@ -486,16 +491,12 @@ export class UnifiedEmailServer extends EventEmitter {
logger.log('info', 'Email delivery queue shut down');
}
// Close all SMTP client connections
for (const [clientKey, client] of this.smtpClients) {
try {
await client.close();
logger.log('info', `Closed SMTP client pool for ${clientKey}`);
} catch (error) {
logger.log('warn', `Error closing SMTP client for ${clientKey}: ${error.message}`);
}
// Close all Rust SMTP client connection pools
try {
await this.rustBridge.closeSmtpPool();
} catch {
// Bridge may already be stopped
}
this.smtpClients.clear();
logger.log('info', 'UnifiedEmailServer stopped successfully');
this.emit('stopped');
@@ -826,13 +827,12 @@ export class UnifiedEmailServer extends EventEmitter {
email.headers['X-Forwarded-To'] = email.to.join(', ');
email.headers['X-Forwarded-Date'] = new Date().toISOString();
// Get SMTP client
const client = this.getSmtpClient(host, port);
try {
// Send email
await client.sendMail(email);
// Send email via Rust SMTP client
await this.sendOutboundEmail(host, port, email, {
auth: auth as { user: string; pass: string } | undefined,
});
logger.log('info', `Successfully forwarded email to ${host}:${port}`);
SecurityLogger.getInstance().logEvent({
@@ -1297,15 +1297,6 @@ export class UnifiedEmailServer extends EventEmitter {
// Queue the email for delivery
await this.deliveryQueue.enqueue(email, mode, route);
// Record 'sent' event for domain reputation monitoring
const senderDomain = email.from.split('@')[1];
if (senderDomain) {
this.recordReputationEvent(senderDomain, {
type: 'sent',
count: email.to.length
});
}
logger.log('info', `Email queued with ID: ${id}`);
return id;
} catch (error) {
@@ -1370,15 +1361,6 @@ export class UnifiedEmailServer extends EventEmitter {
// Notify any registered listeners about the bounce
this.emit('bounceProcessed', bounceRecord);
// Record bounce event for domain reputation tracking
if (bounceRecord.domain) {
this.recordReputationEvent(bounceRecord.domain, {
type: 'bounce',
hardBounce: bounceRecord.bounceCategory === BounceCategory.HARD,
receivingDomain: bounceRecord.recipient.split('@')[1]
});
}
// Log security event
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.INFO,
@@ -1450,15 +1432,6 @@ export class UnifiedEmailServer extends EventEmitter {
// Notify any registered listeners about the bounce
this.emit('bounceProcessed', bounceRecord);
// Record bounce event for domain reputation tracking
if (bounceRecord.domain) {
this.recordReputationEvent(bounceRecord.domain, {
type: 'bounce',
hardBounce: bounceRecord.bounceCategory === BounceCategory.HARD,
receivingDomain: bounceRecord.recipient.split('@')[1]
});
}
// Log security event
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.INFO,
@@ -1566,40 +1539,6 @@ export class UnifiedEmailServer extends EventEmitter {
logger.log('info', `Removed ${email} from suppression list`);
}
/**
* Record an email event for domain reputation tracking.
* Currently a no-op — the sender reputation monitor is not yet implemented.
* @param domain Domain sending the email
* @param event Event details
*/
public recordReputationEvent(domain: string, event: {
type: 'sent' | 'delivered' | 'bounce' | 'complaint' | 'open' | 'click';
count?: number;
hardBounce?: boolean;
receivingDomain?: string;
}): void {
logger.log('debug', `Reputation event for ${domain}: ${event.type}`);
}
/**
* 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
@@ -1608,7 +1547,6 @@ export class UnifiedEmailServer extends EventEmitter {
* @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}`,
@@ -1622,17 +1560,8 @@ export class UnifiedEmailServer extends EventEmitter {
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
});
}
/**