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:
@@ -1,17 +1,14 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import * as net from 'node:net';
|
||||
import * as tls from 'node:tls';
|
||||
import { logger } from '../../logger.js';
|
||||
import {
|
||||
SecurityLogger,
|
||||
SecurityLogLevel,
|
||||
SecurityEventType
|
||||
import {
|
||||
SecurityLogger,
|
||||
SecurityLogLevel,
|
||||
SecurityEventType
|
||||
} from '../../security/index.js';
|
||||
import { UnifiedDeliveryQueue, type IQueueItem } from './classes.delivery.queue.js';
|
||||
import type { Email } from '../core/classes.email.js';
|
||||
import type { UnifiedEmailServer } from '../routing/classes.unified.email.server.js';
|
||||
import type { SmtpClient } from './smtpclient/smtp-client.js';
|
||||
import { RustSecurityBridge } from '../../security/classes.rustsecuritybridge.js';
|
||||
|
||||
/**
|
||||
@@ -117,7 +114,7 @@ export class MultiModeDeliverySystem extends EventEmitter {
|
||||
* Create a new multi-mode delivery system
|
||||
* @param queue Unified delivery queue
|
||||
* @param options Delivery options
|
||||
* @param emailServer Optional reference to unified email server for SmtpClient access
|
||||
* @param emailServer Optional reference to unified email server for outbound delivery
|
||||
*/
|
||||
constructor(queue: UnifiedDeliveryQueue, options: IMultiModeDeliveryOptions, emailServer?: UnifiedEmailServer) {
|
||||
super();
|
||||
@@ -433,191 +430,52 @@ export class MultiModeDeliverySystem extends EventEmitter {
|
||||
*/
|
||||
private async handleForwardDelivery(item: IQueueItem): Promise<any> {
|
||||
logger.log('info', `Forward delivery for item ${item.id}`);
|
||||
|
||||
|
||||
const email = item.processingResult as Email;
|
||||
const route = item.route;
|
||||
|
||||
|
||||
// Get target server information
|
||||
const targetServer = route?.action.forward?.host;
|
||||
const targetPort = route?.action.forward?.port || 25;
|
||||
const useTls = false; // TLS configuration can be enhanced later
|
||||
|
||||
|
||||
if (!targetServer) {
|
||||
throw new Error('No target server configured for forward mode');
|
||||
}
|
||||
|
||||
logger.log('info', `Forwarding email to ${targetServer}:${targetPort}, TLS: ${useTls}`);
|
||||
|
||||
|
||||
logger.log('info', `Forwarding email to ${targetServer}:${targetPort}`);
|
||||
|
||||
try {
|
||||
// Get SMTP client from email server if available
|
||||
if (!this.emailServer) {
|
||||
// Fall back to raw socket implementation if no email server
|
||||
logger.log('warn', 'No email server available, falling back to raw socket implementation');
|
||||
return this.handleForwardDeliveryLegacy(item);
|
||||
throw new Error('No email server available for forward delivery');
|
||||
}
|
||||
|
||||
// Get SMTP client from UnifiedEmailServer
|
||||
const smtpClient = this.emailServer.getSmtpClient(targetServer, targetPort);
|
||||
|
||||
// Apply DKIM signing if configured in the route
|
||||
if (item.route?.action.options?.mtaOptions?.dkimSign) {
|
||||
await this.applyDkimSigning(email, item.route.action.options.mtaOptions);
|
||||
}
|
||||
|
||||
// Send the email using SmtpClient
|
||||
const result = await smtpClient.sendMail(email);
|
||||
|
||||
if (result.success) {
|
||||
logger.log('info', `Email forwarded successfully to ${targetServer}:${targetPort}`);
|
||||
|
||||
return {
|
||||
targetServer: targetServer,
|
||||
targetPort: targetPort,
|
||||
recipients: result.acceptedRecipients.length,
|
||||
messageId: result.messageId,
|
||||
rejectedRecipients: result.rejectedRecipients
|
||||
};
|
||||
} else {
|
||||
throw new Error(result.error?.message || 'Failed to forward email');
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.log('error', `Failed to forward email: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy forward delivery using raw sockets (fallback)
|
||||
* @param item Queue item
|
||||
*/
|
||||
private async handleForwardDeliveryLegacy(item: IQueueItem): Promise<any> {
|
||||
const email = item.processingResult as Email;
|
||||
const route = item.route;
|
||||
|
||||
// Get target server information
|
||||
const targetServer = route?.action.forward?.host;
|
||||
const targetPort = route?.action.forward?.port || 25;
|
||||
const useTls = false; // TLS configuration can be enhanced later
|
||||
|
||||
if (!targetServer) {
|
||||
throw new Error('No target server configured for forward mode');
|
||||
}
|
||||
|
||||
// Create a socket connection to the target server
|
||||
const socket = new net.Socket();
|
||||
|
||||
// Set timeout
|
||||
socket.setTimeout(this.options.socketTimeout);
|
||||
|
||||
try {
|
||||
// Connect to the target server
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
// Handle connection events
|
||||
socket.on('connect', () => {
|
||||
logger.log('debug', `Connected to ${targetServer}:${targetPort}`);
|
||||
resolve();
|
||||
});
|
||||
|
||||
socket.on('timeout', () => {
|
||||
reject(new Error(`Connection timeout to ${targetServer}:${targetPort}`));
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
reject(new Error(`Connection error to ${targetServer}:${targetPort}: ${err.message}`));
|
||||
});
|
||||
|
||||
// Connect to the server
|
||||
socket.connect({
|
||||
host: targetServer,
|
||||
port: targetPort
|
||||
});
|
||||
|
||||
// Build DKIM options from route config
|
||||
const dkimDomain = item.route?.action.options?.mtaOptions?.dkimSign
|
||||
? (item.route.action.options.mtaOptions.dkimOptions?.domainName || email.from.split('@')[1])
|
||||
: undefined;
|
||||
const dkimSelector = item.route?.action.options?.mtaOptions?.dkimOptions?.keySelector || 'default';
|
||||
|
||||
// Build auth options from route forward config
|
||||
const auth = route?.action.forward?.auth as { user: string; pass: string } | undefined;
|
||||
|
||||
// Send via Rust SMTP client
|
||||
const result = await this.emailServer.sendOutboundEmail(targetServer, targetPort, email, {
|
||||
auth,
|
||||
dkimDomain,
|
||||
dkimSelector,
|
||||
});
|
||||
|
||||
// Send EHLO
|
||||
await this.smtpCommand(socket, `EHLO ${route?.action.options?.mtaOptions?.domain || 'localhost'}`);
|
||||
|
||||
// Start TLS if required
|
||||
if (useTls) {
|
||||
await this.smtpCommand(socket, 'STARTTLS');
|
||||
|
||||
// Upgrade to TLS
|
||||
const tlsSocket = await this.upgradeTls(socket, targetServer);
|
||||
|
||||
// Send EHLO again after STARTTLS
|
||||
await this.smtpCommand(tlsSocket, `EHLO ${route?.action.options?.mtaOptions?.domain || 'localhost'}`);
|
||||
|
||||
// Use tlsSocket for remaining commands
|
||||
return this.completeSMTPExchange(tlsSocket, email, route);
|
||||
}
|
||||
|
||||
// Complete the SMTP exchange
|
||||
return this.completeSMTPExchange(socket, email, route);
|
||||
} catch (error: any) {
|
||||
logger.log('error', `Failed to forward email: ${error.message}`);
|
||||
|
||||
// Close the connection
|
||||
socket.destroy();
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete the SMTP exchange after connection and initial setup
|
||||
* @param socket Network socket
|
||||
* @param email Email to send
|
||||
* @param rule Domain rule
|
||||
*/
|
||||
private async completeSMTPExchange(socket: net.Socket | tls.TLSSocket, email: Email, route: any): Promise<any> {
|
||||
try {
|
||||
// Authenticate if credentials provided
|
||||
if (route?.action?.forward?.auth?.user && route?.action?.forward?.auth?.pass) {
|
||||
// Send AUTH LOGIN
|
||||
await this.smtpCommand(socket, 'AUTH LOGIN');
|
||||
|
||||
// Send username (base64)
|
||||
const username = Buffer.from(route.action.forward.auth.user).toString('base64');
|
||||
await this.smtpCommand(socket, username);
|
||||
|
||||
// Send password (base64)
|
||||
const password = Buffer.from(route.action.forward.auth.pass).toString('base64');
|
||||
await this.smtpCommand(socket, password);
|
||||
}
|
||||
|
||||
// Send MAIL FROM
|
||||
await this.smtpCommand(socket, `MAIL FROM:<${email.from}>`);
|
||||
|
||||
// Send RCPT TO for each recipient
|
||||
for (const recipient of email.getAllRecipients()) {
|
||||
await this.smtpCommand(socket, `RCPT TO:<${recipient}>`);
|
||||
}
|
||||
|
||||
// Send DATA
|
||||
await this.smtpCommand(socket, 'DATA');
|
||||
|
||||
// Send email content (simplified)
|
||||
const emailContent = await this.getFormattedEmail(email);
|
||||
await this.smtpData(socket, emailContent);
|
||||
|
||||
// Send QUIT
|
||||
await this.smtpCommand(socket, 'QUIT');
|
||||
|
||||
// Close the connection
|
||||
socket.end();
|
||||
|
||||
logger.log('info', `Email forwarded successfully to ${route?.action?.forward?.host}:${route?.action?.forward?.port || 25}`);
|
||||
|
||||
|
||||
logger.log('info', `Email forwarded successfully to ${targetServer}:${targetPort}`);
|
||||
|
||||
return {
|
||||
targetServer: route?.action?.forward?.host,
|
||||
targetPort: route?.action?.forward?.port || 25,
|
||||
recipients: email.getAllRecipients().length
|
||||
targetServer,
|
||||
targetPort,
|
||||
recipients: result.accepted.length,
|
||||
messageId: result.messageId,
|
||||
rejectedRecipients: result.rejected,
|
||||
};
|
||||
} catch (error: any) {
|
||||
logger.log('error', `Failed to forward email: ${error.message}`);
|
||||
|
||||
// Close the connection
|
||||
socket.destroy();
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -790,210 +648,6 @@ export class MultiModeDeliverySystem extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format email for SMTP transmission
|
||||
* @param email Email to format
|
||||
*/
|
||||
private async getFormattedEmail(email: Email): Promise<string> {
|
||||
// This is a simplified implementation
|
||||
// In a full implementation, this would use proper MIME formatting
|
||||
|
||||
let content = '';
|
||||
|
||||
// Add headers
|
||||
content += `From: ${email.from}\r\n`;
|
||||
content += `To: ${email.to.join(', ')}\r\n`;
|
||||
content += `Subject: ${email.subject}\r\n`;
|
||||
|
||||
// Add additional headers
|
||||
for (const [name, value] of Object.entries(email.headers || {})) {
|
||||
content += `${name}: ${value}\r\n`;
|
||||
}
|
||||
|
||||
// Add content type for multipart
|
||||
if (email.attachments && email.attachments.length > 0) {
|
||||
const boundary = `----_=_NextPart_${Math.random().toString(36).substr(2)}`;
|
||||
content += `MIME-Version: 1.0\r\n`;
|
||||
content += `Content-Type: multipart/mixed; boundary="${boundary}"\r\n`;
|
||||
content += `\r\n`;
|
||||
|
||||
// Add text part
|
||||
content += `--${boundary}\r\n`;
|
||||
content += `Content-Type: text/plain; charset="UTF-8"\r\n`;
|
||||
content += `\r\n`;
|
||||
content += `${email.text}\r\n`;
|
||||
|
||||
// Add HTML part if present
|
||||
if (email.html) {
|
||||
content += `--${boundary}\r\n`;
|
||||
content += `Content-Type: text/html; charset="UTF-8"\r\n`;
|
||||
content += `\r\n`;
|
||||
content += `${email.html}\r\n`;
|
||||
}
|
||||
|
||||
// Add attachments
|
||||
for (const attachment of email.attachments) {
|
||||
content += `--${boundary}\r\n`;
|
||||
content += `Content-Type: ${attachment.contentType || 'application/octet-stream'}; name="${attachment.filename}"\r\n`;
|
||||
content += `Content-Disposition: attachment; filename="${attachment.filename}"\r\n`;
|
||||
content += `Content-Transfer-Encoding: base64\r\n`;
|
||||
content += `\r\n`;
|
||||
|
||||
// Add base64 encoded content
|
||||
const base64Content = attachment.content.toString('base64');
|
||||
|
||||
// Split into lines of 76 characters
|
||||
for (let i = 0; i < base64Content.length; i += 76) {
|
||||
content += base64Content.substring(i, i + 76) + '\r\n';
|
||||
}
|
||||
}
|
||||
|
||||
// End boundary
|
||||
content += `--${boundary}--\r\n`;
|
||||
} else {
|
||||
// Simple email with just text
|
||||
content += `Content-Type: text/plain; charset="UTF-8"\r\n`;
|
||||
content += `\r\n`;
|
||||
content += `${email.text}\r\n`;
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send SMTP command and wait for response
|
||||
* @param socket Socket connection
|
||||
* @param command SMTP command to send
|
||||
*/
|
||||
private async smtpCommand(socket: net.Socket, command: string): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const onData = (data: Buffer) => {
|
||||
const response = data.toString().trim();
|
||||
|
||||
// Clean up listeners
|
||||
socket.removeListener('data', onData);
|
||||
socket.removeListener('error', onError);
|
||||
socket.removeListener('timeout', onTimeout);
|
||||
|
||||
// Check response code
|
||||
if (response.charAt(0) === '2' || response.charAt(0) === '3') {
|
||||
resolve(response);
|
||||
} else {
|
||||
reject(new Error(`SMTP error: ${response}`));
|
||||
}
|
||||
};
|
||||
|
||||
const onError = (err: Error) => {
|
||||
// Clean up listeners
|
||||
socket.removeListener('data', onData);
|
||||
socket.removeListener('error', onError);
|
||||
socket.removeListener('timeout', onTimeout);
|
||||
|
||||
reject(err);
|
||||
};
|
||||
|
||||
const onTimeout = () => {
|
||||
// Clean up listeners
|
||||
socket.removeListener('data', onData);
|
||||
socket.removeListener('error', onError);
|
||||
socket.removeListener('timeout', onTimeout);
|
||||
|
||||
reject(new Error('SMTP command timeout'));
|
||||
};
|
||||
|
||||
// Set up listeners
|
||||
socket.once('data', onData);
|
||||
socket.once('error', onError);
|
||||
socket.once('timeout', onTimeout);
|
||||
|
||||
// Send command
|
||||
socket.write(command + '\r\n');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send SMTP DATA command with content
|
||||
* @param socket Socket connection
|
||||
* @param data Email content to send
|
||||
*/
|
||||
private async smtpData(socket: net.Socket, data: string): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const onData = (responseData: Buffer) => {
|
||||
const response = responseData.toString().trim();
|
||||
|
||||
// Clean up listeners
|
||||
socket.removeListener('data', onData);
|
||||
socket.removeListener('error', onError);
|
||||
socket.removeListener('timeout', onTimeout);
|
||||
|
||||
// Check response code
|
||||
if (response.charAt(0) === '2') {
|
||||
resolve(response);
|
||||
} else {
|
||||
reject(new Error(`SMTP error: ${response}`));
|
||||
}
|
||||
};
|
||||
|
||||
const onError = (err: Error) => {
|
||||
// Clean up listeners
|
||||
socket.removeListener('data', onData);
|
||||
socket.removeListener('error', onError);
|
||||
socket.removeListener('timeout', onTimeout);
|
||||
|
||||
reject(err);
|
||||
};
|
||||
|
||||
const onTimeout = () => {
|
||||
// Clean up listeners
|
||||
socket.removeListener('data', onData);
|
||||
socket.removeListener('error', onError);
|
||||
socket.removeListener('timeout', onTimeout);
|
||||
|
||||
reject(new Error('SMTP data timeout'));
|
||||
};
|
||||
|
||||
// Set up listeners
|
||||
socket.once('data', onData);
|
||||
socket.once('error', onError);
|
||||
socket.once('timeout', onTimeout);
|
||||
|
||||
// Send data and end with CRLF.CRLF
|
||||
socket.write(data + '\r\n.\r\n');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrade socket to TLS
|
||||
* @param socket Socket connection
|
||||
* @param hostname Target hostname for TLS
|
||||
*/
|
||||
private async upgradeTls(socket: net.Socket, hostname: string): Promise<tls.TLSSocket> {
|
||||
return new Promise<tls.TLSSocket>((resolve, reject) => {
|
||||
const tlsOptions: tls.ConnectionOptions = {
|
||||
socket,
|
||||
servername: hostname,
|
||||
rejectUnauthorized: this.options.verifyCertificates,
|
||||
minVersion: this.options.tlsMinVersion as tls.SecureVersion
|
||||
};
|
||||
|
||||
const tlsSocket = tls.connect(tlsOptions);
|
||||
|
||||
tlsSocket.once('secureConnect', () => {
|
||||
resolve(tlsSocket);
|
||||
});
|
||||
|
||||
tlsSocket.once('error', (err) => {
|
||||
reject(new Error(`TLS error: ${err.message}`));
|
||||
});
|
||||
|
||||
tlsSocket.setTimeout(this.options.socketTimeout);
|
||||
|
||||
tlsSocket.once('timeout', () => {
|
||||
reject(new Error('TLS connection timeout'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update delivery time statistics
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user