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

@@ -1,209 +0,0 @@
import { smtpClientMod } from '../../ts/mail/delivery/index.js';
import type { ISmtpClientOptions, SmtpClient } from '../../ts/mail/delivery/smtpclient/index.js';
import { Email } from '../../ts/mail/core/classes.email.js';
/**
* Create a test SMTP client
*/
export function createTestSmtpClient(options: Partial<ISmtpClientOptions> = {}): SmtpClient {
const defaultOptions: ISmtpClientOptions = {
host: options.host || 'localhost',
port: options.port || 2525,
secure: options.secure || false,
auth: options.auth,
connectionTimeout: options.connectionTimeout || 5000,
socketTimeout: options.socketTimeout || 5000,
maxConnections: options.maxConnections || 5,
maxMessages: options.maxMessages || 100,
debug: options.debug || false,
tls: options.tls || {
rejectUnauthorized: false
}
};
return smtpClientMod.createSmtpClient(defaultOptions);
}
/**
* Send test email using SMTP client
*/
export async function sendTestEmail(
client: SmtpClient,
options: {
from?: string;
to?: string | string[];
subject?: string;
text?: string;
html?: string;
} = {}
): Promise<any> {
const mailOptions = {
from: options.from || 'test@example.com',
to: options.to || 'recipient@example.com',
subject: options.subject || 'Test Email',
text: options.text || 'This is a test email',
html: options.html
};
const email = new Email({
from: mailOptions.from,
to: mailOptions.to,
subject: mailOptions.subject,
text: mailOptions.text,
html: mailOptions.html
});
return client.sendMail(email);
}
/**
* Test SMTP client connection
*/
export async function testClientConnection(
host: string,
port: number,
timeout: number = 5000
): Promise<boolean> {
const client = createTestSmtpClient({
host,
port,
connectionTimeout: timeout
});
try {
const result = await client.verify();
return result;
} catch (error) {
throw error;
} finally {
if (client.close) {
await client.close();
}
}
}
/**
* Create authenticated SMTP client
*/
export function createAuthenticatedClient(
host: string,
port: number,
username: string,
password: string,
authMethod: 'PLAIN' | 'LOGIN' = 'PLAIN'
): SmtpClient {
return createTestSmtpClient({
host,
port,
auth: {
user: username,
pass: password,
method: authMethod
},
secure: false
});
}
/**
* Create TLS-enabled SMTP client
*/
export function createTlsClient(
host: string,
port: number,
options: {
secure?: boolean;
rejectUnauthorized?: boolean;
} = {}
): SmtpClient {
return createTestSmtpClient({
host,
port,
secure: options.secure || false,
tls: {
rejectUnauthorized: options.rejectUnauthorized || false
}
});
}
/**
* Test client pool status
*/
export async function testClientPoolStatus(client: SmtpClient): Promise<any> {
if (typeof client.getPoolStatus === 'function') {
return client.getPoolStatus();
}
// Fallback for clients without pool status
return {
size: 1,
available: 1,
pending: 0,
connecting: 0,
active: 0
};
}
/**
* Send multiple emails concurrently
*/
export async function sendConcurrentEmails(
client: SmtpClient,
count: number,
emailOptions: {
from?: string;
to?: string;
subject?: string;
text?: string;
} = {}
): Promise<any[]> {
const promises = [];
for (let i = 0; i < count; i++) {
promises.push(
sendTestEmail(client, {
...emailOptions,
subject: `${emailOptions.subject || 'Test Email'} ${i + 1}`
})
);
}
return Promise.all(promises);
}
/**
* Measure client throughput
*/
export async function measureClientThroughput(
client: SmtpClient,
duration: number = 10000,
emailOptions: {
from?: string;
to?: string;
subject?: string;
text?: string;
} = {}
): Promise<{ totalSent: number; successCount: number; errorCount: number; throughput: number }> {
const startTime = Date.now();
let totalSent = 0;
let successCount = 0;
let errorCount = 0;
while (Date.now() - startTime < duration) {
try {
await sendTestEmail(client, emailOptions);
successCount++;
} catch (error) {
errorCount++;
}
totalSent++;
}
const actualDuration = (Date.now() - startTime) / 1000; // in seconds
const throughput = totalSent / actualDuration;
return {
totalSent,
successCount,
errorCount,
throughput
};
}

View File

@@ -1,154 +0,0 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { smtpClientMod } from '../ts/mail/delivery/index.js';
import type { ISmtpClientOptions, SmtpClient } from '../ts/mail/delivery/smtpclient/index.js';
import { Email } from '../ts/mail/core/classes.email.js';
/**
* Compatibility tests for the legacy SMTP client facade
*/
tap.test('verify backward compatibility - client creation', async () => {
// Create test configuration
const options: ISmtpClientOptions = {
host: 'smtp.example.com',
port: 587,
secure: false,
connectionTimeout: 10000,
domain: 'test.example.com'
};
// Create SMTP client instance using legacy constructor
const smtpClient = smtpClientMod.createSmtpClient(options);
// Verify instance was created correctly
expect(smtpClient).toBeTruthy();
expect(smtpClient.isConnected()).toBeFalsy(); // Should start disconnected
});
tap.test('verify backward compatibility - methods exist', async () => {
const options: ISmtpClientOptions = {
host: 'smtp.example.com',
port: 587,
secure: false
};
const smtpClient = smtpClientMod.createSmtpClient(options);
// Verify all expected methods exist
expect(typeof smtpClient.sendMail === 'function').toBeTruthy();
expect(typeof smtpClient.verify === 'function').toBeTruthy();
expect(typeof smtpClient.isConnected === 'function').toBeTruthy();
expect(typeof smtpClient.getPoolStatus === 'function').toBeTruthy();
expect(typeof smtpClient.updateOptions === 'function').toBeTruthy();
expect(typeof smtpClient.close === 'function').toBeTruthy();
expect(typeof smtpClient.on === 'function').toBeTruthy();
expect(typeof smtpClient.off === 'function').toBeTruthy();
expect(typeof smtpClient.emit === 'function').toBeTruthy();
});
tap.test('verify backward compatibility - options update', async () => {
const options: ISmtpClientOptions = {
host: 'smtp.example.com',
port: 587,
secure: false
};
const smtpClient = smtpClientMod.createSmtpClient(options);
// Test option updates don't throw
expect(() => smtpClient.updateOptions({
host: 'new-smtp.example.com',
port: 465,
secure: true
})).not.toThrow();
expect(() => smtpClient.updateOptions({
debug: true,
connectionTimeout: 5000
})).not.toThrow();
});
tap.test('verify backward compatibility - connection failure handling', async () => {
const options: ISmtpClientOptions = {
host: 'nonexistent.invalid.domain',
port: 587,
secure: false,
connectionTimeout: 1000 // Short timeout for faster test
};
const smtpClient = smtpClientMod.createSmtpClient(options);
// verify() should return false for invalid hosts
const isValid = await smtpClient.verify();
expect(isValid).toBeFalsy();
// sendMail should fail gracefully for invalid hosts
const email = new Email({
from: 'test@example.com',
to: 'recipient@example.com',
subject: 'Test Email',
text: 'This is a test email'
});
try {
const result = await smtpClient.sendMail(email);
expect(result.success).toBeFalsy();
expect(result.error).toBeTruthy();
} catch (error) {
// Connection errors are expected for invalid domains
expect(error).toBeTruthy();
}
});
tap.test('verify backward compatibility - pool status', async () => {
const options: ISmtpClientOptions = {
host: 'smtp.example.com',
port: 587,
secure: false,
pool: true,
maxConnections: 5
};
const smtpClient = smtpClientMod.createSmtpClient(options);
// Get pool status
const status = smtpClient.getPoolStatus();
expect(status).toBeTruthy();
expect(typeof status.total === 'number').toBeTruthy();
expect(typeof status.active === 'number').toBeTruthy();
expect(typeof status.idle === 'number').toBeTruthy();
expect(typeof status.pending === 'number').toBeTruthy();
// Initially should have no connections
expect(status.total).toEqual(0);
expect(status.active).toEqual(0);
expect(status.idle).toEqual(0);
expect(status.pending).toEqual(0);
});
tap.test('verify backward compatibility - event handling', async () => {
const options: ISmtpClientOptions = {
host: 'smtp.example.com',
port: 587,
secure: false
};
const smtpClient = smtpClientMod.createSmtpClient(options);
// Test event listener methods don't throw
const testListener = () => {};
expect(() => smtpClient.on('test', testListener)).not.toThrow();
expect(() => smtpClient.off('test', testListener)).not.toThrow();
expect(() => smtpClient.emit('test')).not.toThrow();
});
tap.test('clean up after compatibility tests', async () => {
// No-op - just to make sure everything is cleaned up properly
});
tap.test('stop', async () => {
await tap.stopForcefully();
});
export default tap.start();

View File

@@ -0,0 +1,107 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { RustSecurityBridge, BridgeState } from '../ts/security/classes.rustsecuritybridge.js';
import type { ISmtpSendOptions, ISmtpPoolStatus } from '../ts/security/classes.rustsecuritybridge.js';
let bridge: RustSecurityBridge;
tap.test('Rust SMTP Client - should start bridge', async () => {
RustSecurityBridge.resetInstance();
bridge = RustSecurityBridge.getInstance();
const ok = await bridge.start();
if (!ok) {
console.log('WARNING: Rust binary not available — skipping Rust SMTP client tests');
console.log('Build it with: cd rust && cargo build --release');
}
expect(typeof ok).toEqual('boolean');
});
tap.test('Rust SMTP Client - getSmtpPoolStatus returns valid structure', async () => {
if (!bridge.running) {
console.log('SKIP: bridge not running');
return;
}
const status: ISmtpPoolStatus = await bridge.getSmtpPoolStatus();
expect(status).toBeTruthy();
expect(status.pools).toBeTruthy();
expect(typeof status.pools).toEqual('object');
});
tap.test('Rust SMTP Client - verifySmtpConnection with unreachable host', async () => {
if (!bridge.running) {
console.log('SKIP: bridge not running');
return;
}
try {
// Use a non-routable IP to test connection failure handling
const result = await bridge.verifySmtpConnection({
host: '192.0.2.1', // TEST-NET-1 (RFC 5737) - guaranteed unreachable
port: 25,
secure: false,
domain: 'test.example.com',
});
// If it returns rather than throwing, reachable should be false
expect(result.reachable).toBeFalse();
} catch (err) {
// Connection errors are expected for unreachable hosts
expect(err).toBeTruthy();
expect(err.message || String(err)).toBeTruthy();
}
});
tap.test('Rust SMTP Client - sendEmail with connection refused error', async () => {
if (!bridge.running) {
console.log('SKIP: bridge not running');
return;
}
const opts: ISmtpSendOptions = {
host: '127.0.0.1',
port: 52599, // random high port - should be refused
secure: false,
domain: 'test.example.com',
email: {
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Test email',
text: 'This is a test.',
},
connectionTimeoutSecs: 5,
socketTimeoutSecs: 5,
};
try {
await bridge.sendOutboundEmail(opts);
// Should not succeed — no server is listening
throw new Error('Expected sendEmail to fail on connection refused');
} catch (err) {
// We expect a connection error
expect(err).toBeTruthy();
const msg = err.message || String(err);
expect(msg.length).toBeGreaterThan(0);
}
});
tap.test('Rust SMTP Client - closeSmtpPool cleans up', async () => {
if (!bridge.running) {
console.log('SKIP: bridge not running');
return;
}
// Should not throw, even when no pools exist
await bridge.closeSmtpPool();
// Verify pool status is empty after close
const status = await bridge.getSmtpPoolStatus();
expect(status.pools).toBeTruthy();
const poolKeys = Object.keys(status.pools);
expect(poolKeys.length).toEqual(0);
});
tap.test('Rust SMTP Client - stop bridge', async () => {
if (!bridge.running) {
console.log('SKIP: bridge not running');
return;
}
await bridge.stop();
expect(bridge.state).toEqual(BridgeState.Stopped);
});
export default tap.start();

View File

@@ -1,191 +0,0 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as plugins from '../ts/plugins.js';
import * as paths from '../ts/paths.js';
import { smtpClientMod } from '../ts/mail/delivery/index.js';
import type { ISmtpClientOptions, SmtpClient } from '../ts/mail/delivery/smtpclient/index.js';
import { Email } from '../ts/mail/core/classes.email.js';
/**
* Tests for the SMTP client class
*/
tap.test('verify SMTP client initialization', async () => {
// Create test configuration
const options: ISmtpClientOptions = {
host: 'smtp.example.com',
port: 587,
secure: false,
connectionTimeout: 10000,
domain: 'test.example.com'
};
// Create SMTP client instance
const smtpClient = smtpClientMod.createSmtpClient(options);
// Verify instance was created correctly
expect(smtpClient).toBeTruthy();
expect(smtpClient.isConnected()).toBeFalsy(); // Should start disconnected
});
tap.test('test SMTP client configuration update', async () => {
// Create test configuration
const options: ISmtpClientOptions = {
host: 'smtp.example.com',
port: 587,
secure: false
};
// Create SMTP client instance
const smtpClient = smtpClientMod.createSmtpClient(options);
// Update configuration
smtpClient.updateOptions({
host: 'new-smtp.example.com',
port: 465,
secure: true
});
// Can't directly test private fields, but we can verify it doesn't throw
expect(() => smtpClient.updateOptions({
tls: {
rejectUnauthorized: false
}
})).not.toThrow();
});
// Mocked SMTP server for testing
class MockSmtpServer {
private responses: Map<string, string>;
constructor() {
this.responses = new Map();
// Default responses
this.responses.set('connect', '220 smtp.example.com ESMTP ready');
this.responses.set('EHLO', '250-smtp.example.com\r\n250-PIPELINING\r\n250-SIZE 10240000\r\n250-STARTTLS\r\n250-AUTH PLAIN LOGIN\r\n250 HELP');
this.responses.set('MAIL FROM', '250 OK');
this.responses.set('RCPT TO', '250 OK');
this.responses.set('DATA', '354 Start mail input; end with <CRLF>.<CRLF>');
this.responses.set('data content', '250 OK: message accepted');
this.responses.set('QUIT', '221 Bye');
}
public setResponse(command: string, response: string): void {
this.responses.set(command, response);
}
public getResponse(command: string): string {
if (command.startsWith('MAIL FROM')) {
return this.responses.get('MAIL FROM') || '250 OK';
} else if (command.startsWith('RCPT TO')) {
return this.responses.get('RCPT TO') || '250 OK';
} else if (command.startsWith('EHLO') || command.startsWith('HELO')) {
return this.responses.get('EHLO') || '250 OK';
} else if (command === 'DATA') {
return this.responses.get('DATA') || '354 Start mail input; end with <CRLF>.<CRLF>';
} else if (command.includes('Content-Type')) {
return this.responses.get('data content') || '250 OK: message accepted';
} else if (command === 'QUIT') {
return this.responses.get('QUIT') || '221 Bye';
}
return this.responses.get(command) || '250 OK';
}
}
/**
* This test validates the SMTP client public interface
*/
tap.test('verify SMTP client email delivery functionality with mock', async () => {
// Create a test email
const testEmail = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Test Email',
text: 'This is a test email'
});
// Create SMTP client options
const options: ISmtpClientOptions = {
host: 'smtp.example.com',
port: 587,
secure: false,
domain: 'test.example.com',
auth: {
user: 'testuser',
pass: 'testpass'
}
};
// Create SMTP client instance
const smtpClient = smtpClientMod.createSmtpClient(options);
// Test public methods exist and have correct signatures
expect(typeof smtpClient.sendMail).toEqual('function');
expect(typeof smtpClient.verify).toEqual('function');
expect(typeof smtpClient.isConnected).toEqual('function');
expect(typeof smtpClient.getPoolStatus).toEqual('function');
expect(typeof smtpClient.updateOptions).toEqual('function');
expect(typeof smtpClient.close).toEqual('function');
// Test connection status before any operation
expect(smtpClient.isConnected()).toBeFalsy();
// Test pool status
const poolStatus = smtpClient.getPoolStatus();
expect(poolStatus).toBeTruthy();
expect(typeof poolStatus.active).toEqual('number');
expect(typeof poolStatus.idle).toEqual('number');
expect(typeof poolStatus.total).toEqual('number');
// Since we can't connect to a real server, we'll skip the actual send test
// and just verify the client was created correctly
expect(smtpClient).toBeTruthy();
});
tap.test('test SMTP client error handling with mock', async () => {
// Create SMTP client instance
const smtpClient = smtpClientMod.createSmtpClient({
host: 'smtp.example.com',
port: 587,
secure: false
});
// Test with valid email (Email class might allow any string)
const testEmail = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Test Email',
text: 'This is a test email'
});
// Test event listener methods
const mockListener = () => {};
smtpClient.on('test-event', mockListener);
smtpClient.off('test-event', mockListener);
// Test update options
smtpClient.updateOptions({
auth: {
user: 'newuser',
pass: 'newpass'
}
});
// Verify client is still functional
expect(smtpClient.isConnected()).toBeFalsy();
// Test close on a non-connected client
await smtpClient.close();
expect(smtpClient.isConnected()).toBeFalsy();
});
// Final clean-up test
tap.test('clean up after tests', async () => {
// No-op - just to make sure everything is cleaned up properly
});
tap.test('stop', async () => {
await tap.stopForcefully();
});
export default tap.start();