BREAKING CHANGE(smartmta): Rebrand package to @push.rocks/smartmta, add consolidated email security verification and IPC handler
This commit is contained in:
307
ts/security/classes.rustsecuritybridge.ts
Normal file
307
ts/security/classes.rustsecuritybridge.ts
Normal 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,
|
||||
};
|
||||
Reference in New Issue
Block a user