BREAKING CHANGE(smartmta): Rebrand package to @push.rocks/smartmta, add consolidated email security verification and IPC handler

This commit is contained in:
2026-02-10 16:25:55 +00:00
parent 199b9b79d2
commit 8293663619
17 changed files with 1183 additions and 383 deletions

View File

@@ -2,7 +2,7 @@
* autocreated commitinfo by @push.rocks/commitinfo
*/
export const commitinfo = {
name: '@serve.zone/mailer',
version: '1.3.1',
description: 'Enterprise mail server with SMTP, HTTP API, and DNS management - built for serve.zone infrastructure'
name: '@push.rocks/smartmta',
version: '2.0.0',
description: 'A high-performance, enterprise-grade Mail Transfer Agent (MTA) built from scratch in TypeScript with Rust acceleration.'
}

View File

@@ -9,6 +9,7 @@ import {
} from '../../security/index.js';
import { DKIMCreator } from '../security/classes.dkimcreator.js';
import { IPReputationChecker } from '../../security/classes.ipreputationchecker.js';
import { RustSecurityBridge } from '../../security/classes.rustsecuritybridge.js';
// Deliverability types (IPWarmupManager and SenderReputationMonitor are optional external modules)
interface IIPWarmupConfig {
enabled?: boolean;
@@ -192,7 +193,8 @@ export class UnifiedEmailServer extends EventEmitter {
// Add components needed for sending and securing emails
public dkimCreator: DKIMCreator;
private ipReputationChecker: IPReputationChecker; // TODO: Implement IP reputation checks in processEmailByMode
private rustBridge: RustSecurityBridge;
private ipReputationChecker: IPReputationChecker;
private bounceManager: BounceManager;
private ipWarmupManager: IPWarmupManager | null;
private senderReputationMonitor: SenderReputationMonitor | null;
@@ -217,6 +219,9 @@ export class UnifiedEmailServer extends EventEmitter {
socketTimeout: options.socketTimeout || 60000 // 1 minute
};
// Initialize Rust security bridge (singleton)
this.rustBridge = RustSecurityBridge.getInstance();
// Initialize DKIM creator with storage manager
this.dkimCreator = new DKIMCreator(paths.keysDir, dcRouter.storageManager);
@@ -360,7 +365,15 @@ export class UnifiedEmailServer extends EventEmitter {
// Start the delivery system
await this.deliverySystem.start();
logger.log('info', 'Email delivery system started');
// Start Rust security bridge (non-blocking — server works without it)
const bridgeOk = await this.rustBridge.start();
if (bridgeOk) {
logger.log('info', 'Rust security bridge started — using Rust for DKIM/SPF/DMARC verification');
} else {
logger.log('warn', 'Rust security bridge unavailable — falling back to TypeScript security verification');
}
// Set up DKIM for all domains
await this.setupDkimForDomains();
logger.log('info', 'DKIM configuration completed for all domains');
@@ -417,12 +430,40 @@ export class UnifiedEmailServer extends EventEmitter {
verifyDmarc: true
}
},
// These will be implemented in the real integration:
// Security verification delegated to the Rust bridge when available
dkimVerifier: {
verify: async () => ({ isValid: true, domain: '' })
verify: async (rawMessage: string) => {
if (this.rustBridge.running) {
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, accepting: ${(err as Error).message}`);
return { isValid: true, domain: '' };
}
}
return { isValid: true, domain: '' }; // No bridge — accept
}
},
spfVerifier: {
verifyAndApply: async () => true
verifyAndApply: async (session: any) => {
if (this.rustBridge.running && session?.remoteAddress && session.remoteAddress !== '127.0.0.1') {
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, accepting: ${(err as Error).message}`);
return true;
}
}
return true; // No bridge or localhost — accept
}
},
dmarcVerifier: {
verify: async () => ({}),
@@ -552,7 +593,10 @@ export class UnifiedEmailServer extends EventEmitter {
try {
// Clear the servers array - servers will be garbage collected
this.servers = [];
// Stop Rust security bridge
await this.rustBridge.stop();
// Stop the delivery system
if (this.deliverySystem) {
await this.deliverySystem.stop();
@@ -588,6 +632,63 @@ export class UnifiedEmailServer extends EventEmitter {
/**
* Verify inbound email security (DKIM/SPF/DMARC) using the Rust bridge.
* Falls back gracefully if the bridge is not running.
*/
private async verifyInboundSecurity(email: Email, session: IExtendedSmtpSession): Promise<void> {
if (!this.rustBridge.running) {
return; // Bridge not available — skip verification
}
try {
const rawMessage = session.emailData || email.toRFC822String();
const result = await this.rustBridge.verifyEmail({
rawMessage,
ip: session.remoteAddress,
heloDomain: session.clientHostname || '',
hostname: this.options.hostname,
mailFrom: session.envelope?.mailFrom?.address || session.mailFrom || '',
});
// Apply DKIM result headers
if (result.dkim && result.dkim.length > 0) {
const dkimSummary = result.dkim
.map(d => `${d.status}${d.domain ? ` (${d.domain})` : ''}`)
.join(', ');
email.setHeader('X-DKIM-Result', dkimSummary);
}
// Apply SPF result header
if (result.spf) {
email.setHeader('Received-SPF', `${result.spf.result} (domain: ${result.spf.domain}, ip: ${result.spf.ip})`);
// Mark as spam on SPF hard fail
if (result.spf.result === 'fail') {
email.mightBeSpam = true;
logger.log('warn', `SPF fail for ${session.remoteAddress} — marking as potential spam`);
}
}
// Apply DMARC result header and policy
if (result.dmarc) {
email.setHeader('X-DMARC-Result', `${result.dmarc.action} (policy=${result.dmarc.policy}, dkim=${result.dmarc.dkim_result}, spf=${result.dmarc.spf_result})`);
if (result.dmarc.action === 'reject') {
email.mightBeSpam = true;
logger.log('warn', `DMARC reject for domain ${result.dmarc.domain} — marking as spam`);
} else if (result.dmarc.action === 'quarantine') {
email.mightBeSpam = true;
logger.log('info', `DMARC quarantine for domain ${result.dmarc.domain} — marking as potential spam`);
}
}
logger.log('info', `Inbound security verified for email from ${session.remoteAddress}: DKIM=${result.dkim?.[0]?.status ?? 'none'}, SPF=${result.spf?.result ?? 'none'}, DMARC=${result.dmarc?.action ?? 'none'}`);
} catch (err) {
logger.log('warn', `Inbound security verification failed: ${(err as Error).message} — accepting email`);
}
}
/**
* Process email based on routing rules
*/
@@ -617,7 +718,12 @@ export class UnifiedEmailServer extends EventEmitter {
} else {
email = emailData;
}
// Run inbound security verification (DKIM/SPF/DMARC) via Rust bridge
if (session.remoteAddress && session.remoteAddress !== '127.0.0.1') {
await this.verifyInboundSecurity(email, session);
}
// First check if this is a bounce notification email
// Look for common bounce notification subject patterns
const subject = email.subject || '';

View File

@@ -58,12 +58,13 @@ import * as smartproxy from '@push.rocks/smartproxy';
import * as smartpromise from '@push.rocks/smartpromise';
import * as smartrequest from '@push.rocks/smartrequest';
import * as smartrule from '@push.rocks/smartrule';
import * as smartrust from '@push.rocks/smartrust';
import * as smartrx from '@push.rocks/smartrx';
import * as smartunique from '@push.rocks/smartunique';
export const smartfs = new SmartFs(new SmartFsProviderNode());
export { projectinfo, qenv, smartacme, smartdata, smartdns, smartfile, SmartFs, smartguard, smartjwt, smartlog, smartmail, smartmetrics, smartnetwork, smartpath, smartproxy, smartpromise, smartrequest, smartrule, smartrx, smartunique };
export { projectinfo, qenv, smartacme, smartdata, smartdns, smartfile, SmartFs, smartguard, smartjwt, smartlog, smartmail, smartmetrics, smartnetwork, smartpath, smartproxy, smartpromise, smartrequest, smartrule, smartrust, smartrx, smartunique };
// Define SmartLog types for use in error handling
export type TLogLevel = 'error' | 'warn' | 'info' | 'success' | 'debug';

View File

@@ -2,6 +2,7 @@ import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
import { logger } from '../logger.js';
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from './classes.securitylogger.js';
import { RustSecurityBridge } from './classes.rustsecuritybridge.js';
import { LRUCache } from 'lru-cache';
/**
@@ -156,7 +157,7 @@ export class IPReputationChecker {
logger.log('warn', `Invalid IP address format: ${ip}`);
return this.createErrorResult(ip, 'Invalid IP address format');
}
// Check cache first
const cachedResult = this.reputationCache.get(ip);
if (cachedResult) {
@@ -166,8 +167,37 @@ export class IPReputationChecker {
});
return cachedResult;
}
// Initialize empty result
// Try Rust bridge first (parallel DNSBL via tokio — faster than Node sequential DNS)
const bridge = RustSecurityBridge.getInstance();
if (bridge.running) {
try {
const rustResult = await bridge.checkIpReputation(ip);
const result: IReputationResult = {
score: rustResult.score,
isSpam: rustResult.listed_count > 0,
isProxy: rustResult.ip_type === 'proxy',
isTor: rustResult.ip_type === 'tor',
isVPN: rustResult.ip_type === 'vpn',
blacklists: rustResult.dnsbl_results
.filter(d => d.listed)
.map(d => d.server),
timestamp: Date.now(),
};
this.reputationCache.set(ip, result);
if (this.options.enableLocalCache) {
this.saveCache().catch(error => {
logger.log('error', `Failed to save IP reputation cache: ${error.message}`);
});
}
this.logReputationCheck(ip, result);
return result;
} catch (err) {
logger.log('warn', `Rust IP reputation check failed, falling back to TS: ${(err as Error).message}`);
}
}
// Fallback: TypeScript DNSBL implementation
const result: IReputationResult = {
score: 100, // Start with perfect score
isSpam: false,
@@ -176,43 +206,43 @@ export class IPReputationChecker {
isVPN: false,
timestamp: Date.now()
};
// Check IP against DNS blacklists if enabled
if (this.options.enableDNSBL) {
const dnsblResult = await this.checkDNSBL(ip);
// Update result with DNSBL information
result.score -= dnsblResult.listCount * 10; // Subtract 10 points per blacklist
result.isSpam = dnsblResult.listCount > 0;
result.blacklists = dnsblResult.lists;
}
// Get additional IP information if enabled
if (this.options.enableIPInfo) {
const ipInfo = await this.getIPInfo(ip);
// Update result with IP info
result.country = ipInfo.country;
result.asn = ipInfo.asn;
result.org = ipInfo.org;
// Adjust score based on IP type
if (ipInfo.type === IPType.PROXY || ipInfo.type === IPType.TOR || ipInfo.type === IPType.VPN) {
result.score -= 30; // Subtract 30 points for proxies, Tor, VPNs
// Set proxy flags
result.isProxy = ipInfo.type === IPType.PROXY;
result.isTor = ipInfo.type === IPType.TOR;
result.isVPN = ipInfo.type === IPType.VPN;
}
}
// Ensure score is between 0 and 100
result.score = Math.max(0, Math.min(100, result.score));
// Update cache with result
this.reputationCache.set(ip, result);
// Save cache if enabled
if (this.options.enableLocalCache) {
// Fire and forget the save operation
@@ -220,17 +250,17 @@ export class IPReputationChecker {
logger.log('error', `Failed to save IP reputation cache: ${error.message}`);
});
}
// Log the reputation check
this.logReputationCheck(ip, result);
return result;
} catch (error) {
logger.log('error', `Error checking IP reputation for ${ip}: ${error.message}`, {
ip,
stack: error.stack
});
return this.createErrorResult(ip, error.message);
}
}

View File

@@ -0,0 +1,307 @@
import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
import { logger } from '../logger.js';
// ---------------------------------------------------------------------------
// IPC command type map — mirrors the methods in mailer-bin's management mode
// ---------------------------------------------------------------------------
interface IDkimVerificationResult {
is_valid: boolean;
domain: string | null;
selector: string | null;
status: string;
details: string | null;
}
interface ISpfResult {
result: string;
domain: string;
ip: string;
explanation: string | null;
}
interface IDmarcResult {
passed: boolean;
policy: string;
domain: string;
dkim_result: string;
spf_result: string;
action: string;
details: string | null;
}
interface IEmailSecurityResult {
dkim: IDkimVerificationResult[];
spf: ISpfResult | null;
dmarc: IDmarcResult | null;
}
interface IValidationResult {
valid: boolean;
formatValid: boolean;
score: number;
error: string | null;
}
interface IBounceDetection {
bounce_type: string;
severity: string;
category: string;
should_retry: boolean;
recommended_action: string;
details: string;
}
interface IReputationResult {
ip: string;
score: number;
risk_level: string;
ip_type: string;
dnsbl_results: Array<{ server: string; listed: boolean; response: string | null }>;
listed_count: number;
total_checked: number;
}
interface IVersionInfo {
bin: string;
core: string;
security: string;
smtp: string;
}
/**
* Type-safe command map for the mailer-bin IPC bridge.
*/
type TMailerCommands = {
ping: {
params: Record<string, never>;
result: { pong: boolean };
};
version: {
params: Record<string, never>;
result: IVersionInfo;
};
validateEmail: {
params: { email: string };
result: IValidationResult;
};
detectBounce: {
params: { smtpResponse?: string; diagnosticCode?: string; statusCode?: string };
result: IBounceDetection;
};
checkIpReputation: {
params: { ip: string };
result: IReputationResult;
};
verifyDkim: {
params: { rawMessage: string };
result: IDkimVerificationResult[];
};
signDkim: {
params: { rawMessage: string; domain: string; selector?: string; privateKey: string };
result: { header: string; signedMessage: string };
};
checkSpf: {
params: { ip: string; heloDomain: string; hostname?: string; mailFrom: string };
result: ISpfResult;
};
verifyEmail: {
params: {
rawMessage: string;
ip: string;
heloDomain: string;
hostname?: string;
mailFrom: string;
};
result: IEmailSecurityResult;
};
};
// ---------------------------------------------------------------------------
// RustSecurityBridge — singleton wrapper around smartrust.RustBridge
// ---------------------------------------------------------------------------
/**
* Bridge between TypeScript and the Rust `mailer-bin` binary.
*
* Uses `@push.rocks/smartrust` for JSON-over-stdin/stdout IPC.
* Singleton — access via `RustSecurityBridge.getInstance()`.
*/
export class RustSecurityBridge {
private static instance: RustSecurityBridge | null = null;
private bridge: InstanceType<typeof plugins.smartrust.RustBridge<TMailerCommands>>;
private _running = false;
private constructor() {
this.bridge = new plugins.smartrust.RustBridge<TMailerCommands>({
binaryName: 'mailer-bin',
cliArgs: ['--management'],
requestTimeoutMs: 30_000,
readyTimeoutMs: 10_000,
localPaths: [
plugins.path.join(paths.packageDir, 'dist_rust', 'mailer-bin'),
plugins.path.join(paths.packageDir, 'rust', 'target', 'release', 'mailer-bin'),
plugins.path.join(paths.packageDir, 'rust', 'target', 'debug', 'mailer-bin'),
],
searchSystemPath: false,
});
// Forward lifecycle events
this.bridge.on('ready', () => {
this._running = true;
logger.log('info', 'Rust security bridge is ready');
});
this.bridge.on('exit', (code: number | null, signal: string | null) => {
this._running = false;
logger.log('warn', `Rust security bridge exited (code=${code}, signal=${signal})`);
});
this.bridge.on('stderr', (line: string) => {
logger.log('debug', `[rust-bridge] ${line}`);
});
}
/** Get or create the singleton instance. */
public static getInstance(): RustSecurityBridge {
if (!RustSecurityBridge.instance) {
RustSecurityBridge.instance = new RustSecurityBridge();
}
return RustSecurityBridge.instance;
}
/** Whether the Rust process is currently running and accepting commands. */
public get running(): boolean {
return this._running;
}
// -----------------------------------------------------------------------
// Lifecycle
// -----------------------------------------------------------------------
/**
* Spawn the Rust binary and wait for the ready signal.
* @returns `true` if the binary started successfully, `false` otherwise.
*/
public async start(): Promise<boolean> {
if (this._running) {
return true;
}
try {
const ok = await this.bridge.spawn();
this._running = ok;
if (ok) {
logger.log('info', 'Rust security bridge started');
} else {
logger.log('warn', 'Rust security bridge failed to start (binary not found or timeout)');
}
return ok;
} catch (err) {
logger.log('error', `Failed to start Rust security bridge: ${(err as Error).message}`);
return false;
}
}
/** Kill the Rust process. */
public async stop(): Promise<void> {
if (!this._running) {
return;
}
try {
this.bridge.kill();
this._running = false;
logger.log('info', 'Rust security bridge stopped');
} catch (err) {
logger.log('error', `Error stopping Rust security bridge: ${(err as Error).message}`);
}
}
// -----------------------------------------------------------------------
// Commands — thin typed wrappers over sendCommand
// -----------------------------------------------------------------------
/** Ping the Rust process. */
public async ping(): Promise<boolean> {
const res = await this.bridge.sendCommand('ping', {} as any);
return res?.pong === true;
}
/** Get version information for all Rust crates. */
public async getVersion(): Promise<IVersionInfo> {
return this.bridge.sendCommand('version', {} as any);
}
/** Validate an email address. */
public async validateEmail(email: string): Promise<IValidationResult> {
return this.bridge.sendCommand('validateEmail', { email });
}
/** Detect bounce type from SMTP response / diagnostic code. */
public async detectBounce(opts: {
smtpResponse?: string;
diagnosticCode?: string;
statusCode?: string;
}): Promise<IBounceDetection> {
return this.bridge.sendCommand('detectBounce', opts);
}
/** Check IP reputation via DNSBL. */
public async checkIpReputation(ip: string): Promise<IReputationResult> {
return this.bridge.sendCommand('checkIpReputation', { ip });
}
/** Verify DKIM signatures on a raw email message. */
public async verifyDkim(rawMessage: string): Promise<IDkimVerificationResult[]> {
return this.bridge.sendCommand('verifyDkim', { rawMessage });
}
/** Sign an email with DKIM. */
public async signDkim(opts: {
rawMessage: string;
domain: string;
selector?: string;
privateKey: string;
}): Promise<{ header: string; signedMessage: string }> {
return this.bridge.sendCommand('signDkim', opts);
}
/** Check SPF for a sender. */
public async checkSpf(opts: {
ip: string;
heloDomain: string;
hostname?: string;
mailFrom: string;
}): Promise<ISpfResult> {
return this.bridge.sendCommand('checkSpf', opts);
}
/**
* Compound email security verification: DKIM + SPF + DMARC in one IPC call.
*
* This is the preferred method for inbound email verification — it avoids
* 3 sequential round-trips and correctly passes raw mail-auth types internally.
*/
public async verifyEmail(opts: {
rawMessage: string;
ip: string;
heloDomain: string;
hostname?: string;
mailFrom: string;
}): Promise<IEmailSecurityResult> {
return this.bridge.sendCommand('verifyEmail', opts);
}
}
// Re-export interfaces for consumers
export type {
IDkimVerificationResult,
ISpfResult,
IDmarcResult,
IEmailSecurityResult,
IValidationResult,
IBounceDetection,
IReputationResult as IRustReputationResult,
IVersionInfo,
};

View File

@@ -18,4 +18,16 @@ export {
ThreatCategory,
type IScanResult,
type IContentScannerOptions
} from './classes.contentscanner.js';
} from './classes.contentscanner.js';
export {
RustSecurityBridge,
type IDkimVerificationResult,
type ISpfResult,
type IDmarcResult,
type IEmailSecurityResult,
type IValidationResult,
type IBounceDetection,
type IRustReputationResult,
type IVersionInfo,
} from './classes.rustsecuritybridge.js';