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:
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user