944 lines
28 KiB
TypeScript
944 lines
28 KiB
TypeScript
import * as plugins from '../plugins.js';
|
|
import * as paths from '../paths.js';
|
|
import { logger } from '../logger.js';
|
|
import { EventEmitter } from 'events';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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;
|
|
category: 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 IContentScanResult {
|
|
threatScore: number;
|
|
threatType: string | null;
|
|
threatDetails: string | null;
|
|
scannedElements: string[];
|
|
}
|
|
|
|
// --- SMTP Client types ---
|
|
|
|
interface IOutboundEmail {
|
|
from: string;
|
|
to: string[];
|
|
cc?: string[];
|
|
bcc?: string[];
|
|
subject?: string;
|
|
text?: string;
|
|
html?: string;
|
|
headers?: Record<string, string>;
|
|
}
|
|
|
|
interface ISmtpSendResult {
|
|
accepted: string[];
|
|
rejected: string[];
|
|
messageId?: string;
|
|
response: string;
|
|
envelope: { from: string; to: string[] };
|
|
}
|
|
|
|
interface ISmtpSendOptions {
|
|
host: string;
|
|
port: number;
|
|
secure?: boolean;
|
|
domain?: string;
|
|
auth?: { user: string; pass: string; method?: string };
|
|
email: IOutboundEmail;
|
|
dkim?: { domain: string; selector: string; privateKey: string; keyType?: string };
|
|
connectionTimeoutSecs?: number;
|
|
socketTimeoutSecs?: number;
|
|
poolKey?: string;
|
|
maxPoolConnections?: number;
|
|
tlsOpportunistic?: boolean;
|
|
}
|
|
|
|
interface ISmtpSendRawOptions {
|
|
host: string;
|
|
port: number;
|
|
secure?: boolean;
|
|
domain?: string;
|
|
auth?: { user: string; pass: string; method?: string };
|
|
envelopeFrom: string;
|
|
envelopeTo: string[];
|
|
rawMessageBase64: string;
|
|
poolKey?: string;
|
|
}
|
|
|
|
interface ISmtpVerifyOptions {
|
|
host: string;
|
|
port: number;
|
|
secure?: boolean;
|
|
domain?: string;
|
|
auth?: { user: string; pass: string; method?: string };
|
|
}
|
|
|
|
interface ISmtpVerifyResult {
|
|
reachable: boolean;
|
|
greeting?: string;
|
|
capabilities?: string[];
|
|
}
|
|
|
|
interface ISmtpPoolStatus {
|
|
pools: Record<string, { total: number; active: number; idle: number }>;
|
|
}
|
|
|
|
interface IVersionInfo {
|
|
bin: string;
|
|
core: string;
|
|
security: string;
|
|
smtp: string;
|
|
}
|
|
|
|
// --- SMTP Server types ---
|
|
|
|
interface ISmtpServerConfig {
|
|
hostname: string;
|
|
ports: number[];
|
|
securePort?: number;
|
|
tlsCertPem?: string;
|
|
tlsKeyPem?: string;
|
|
additionalTlsCerts?: Array<{ domains: string[]; certPem: string; keyPem: string }>;
|
|
maxMessageSize?: number;
|
|
maxConnections?: number;
|
|
maxRecipients?: number;
|
|
connectionTimeoutSecs?: number;
|
|
dataTimeoutSecs?: number;
|
|
authEnabled?: boolean;
|
|
maxAuthFailures?: number;
|
|
socketTimeoutSecs?: number;
|
|
processingTimeoutSecs?: number;
|
|
rateLimits?: IRateLimitConfig;
|
|
}
|
|
|
|
interface IRateLimitConfig {
|
|
maxConnectionsPerIp?: number;
|
|
maxMessagesPerSender?: number;
|
|
maxAuthFailuresPerIp?: number;
|
|
windowSecs?: number;
|
|
}
|
|
|
|
interface IEmailData {
|
|
type: 'inline' | 'file';
|
|
base64?: string;
|
|
path?: string;
|
|
}
|
|
|
|
interface IEmailReceivedEvent {
|
|
correlationId: string;
|
|
sessionId: string;
|
|
mailFrom: string;
|
|
rcptTo: string[];
|
|
data: IEmailData;
|
|
remoteAddr: string;
|
|
clientHostname: string | null;
|
|
secure: boolean;
|
|
authenticatedUser: string | null;
|
|
securityResults: any | null;
|
|
}
|
|
|
|
interface IAuthRequestEvent {
|
|
correlationId: string;
|
|
sessionId: string;
|
|
username: string;
|
|
password: string;
|
|
remoteAddr: string;
|
|
}
|
|
|
|
interface IScramCredentialRequestEvent {
|
|
correlationId: string;
|
|
sessionId: string;
|
|
username: string;
|
|
remoteAddr: 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; keyType?: string };
|
|
result: { header: string; signedMessage: string };
|
|
};
|
|
checkSpf: {
|
|
params: { ip: string; heloDomain: string; hostname?: string; mailFrom: string };
|
|
result: ISpfResult;
|
|
};
|
|
scanContent: {
|
|
params: {
|
|
subject?: string;
|
|
textBody?: string;
|
|
htmlBody?: string;
|
|
attachmentNames?: string[];
|
|
};
|
|
result: IContentScanResult;
|
|
};
|
|
verifyEmail: {
|
|
params: {
|
|
rawMessage: string;
|
|
ip: string;
|
|
heloDomain: string;
|
|
hostname?: string;
|
|
mailFrom: string;
|
|
};
|
|
result: IEmailSecurityResult;
|
|
};
|
|
startSmtpServer: {
|
|
params: ISmtpServerConfig;
|
|
result: { started: boolean };
|
|
};
|
|
stopSmtpServer: {
|
|
params: Record<string, never>;
|
|
result: { stopped: boolean; wasRunning?: boolean };
|
|
};
|
|
emailProcessingResult: {
|
|
params: {
|
|
correlationId: string;
|
|
accepted: boolean;
|
|
smtpCode?: number;
|
|
smtpMessage?: string;
|
|
};
|
|
result: { resolved: boolean };
|
|
};
|
|
authResult: {
|
|
params: {
|
|
correlationId: string;
|
|
success: boolean;
|
|
message?: string;
|
|
};
|
|
result: { resolved: boolean };
|
|
};
|
|
scramCredentialResult: {
|
|
params: {
|
|
correlationId: string;
|
|
found: boolean;
|
|
salt?: string;
|
|
iterations?: number;
|
|
storedKey?: string;
|
|
serverKey?: string;
|
|
};
|
|
result: { resolved: boolean };
|
|
};
|
|
configureRateLimits: {
|
|
params: IRateLimitConfig;
|
|
result: { configured: boolean };
|
|
};
|
|
sendEmail: {
|
|
params: ISmtpSendOptions;
|
|
result: ISmtpSendResult;
|
|
};
|
|
sendRawEmail: {
|
|
params: ISmtpSendRawOptions;
|
|
result: ISmtpSendResult;
|
|
};
|
|
verifySmtpConnection: {
|
|
params: ISmtpVerifyOptions;
|
|
result: ISmtpVerifyResult;
|
|
};
|
|
closeSmtpPool: {
|
|
params: { poolKey?: string };
|
|
result: { closed: boolean };
|
|
};
|
|
getSmtpPoolStatus: {
|
|
params: Record<string, never>;
|
|
result: ISmtpPoolStatus;
|
|
};
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Bridge state machine
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export enum BridgeState {
|
|
Idle = 'idle',
|
|
Starting = 'starting',
|
|
Running = 'running',
|
|
Restarting = 'restarting',
|
|
Failed = 'failed',
|
|
Stopped = 'stopped',
|
|
}
|
|
|
|
export interface IBridgeResilienceConfig {
|
|
maxRestartAttempts: number;
|
|
healthCheckIntervalMs: number;
|
|
restartBackoffBaseMs: number;
|
|
restartBackoffMaxMs: number;
|
|
healthCheckTimeoutMs: number;
|
|
}
|
|
|
|
const DEFAULT_RESILIENCE_CONFIG: IBridgeResilienceConfig = {
|
|
maxRestartAttempts: 5,
|
|
healthCheckIntervalMs: 30_000,
|
|
restartBackoffBaseMs: 1_000,
|
|
restartBackoffMaxMs: 30_000,
|
|
healthCheckTimeoutMs: 5_000,
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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()`.
|
|
*
|
|
* Features resilience via auto-restart with exponential backoff,
|
|
* periodic health checks, and a state machine that tracks the
|
|
* bridge lifecycle.
|
|
*/
|
|
export class RustSecurityBridge extends EventEmitter {
|
|
private static instance: RustSecurityBridge | null = null;
|
|
private static _resilienceConfig: IBridgeResilienceConfig = { ...DEFAULT_RESILIENCE_CONFIG };
|
|
|
|
private bridge: InstanceType<typeof plugins.smartrust.RustBridge<TMailerCommands>>;
|
|
private _running = false;
|
|
private _state: BridgeState = BridgeState.Idle;
|
|
private _restartAttempts = 0;
|
|
private _restartTimer: ReturnType<typeof setTimeout> | null = null;
|
|
private _healthCheckTimer: ReturnType<typeof setInterval> | null = null;
|
|
private _deliberateStop = false;
|
|
private _smtpServerConfig: ISmtpServerConfig | null = null;
|
|
|
|
private constructor() {
|
|
super();
|
|
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})`);
|
|
|
|
if (this._deliberateStop) {
|
|
this.setState(BridgeState.Stopped);
|
|
} else if (this._state === BridgeState.Running) {
|
|
// Unexpected exit — attempt restart
|
|
this.attemptRestart();
|
|
}
|
|
});
|
|
|
|
this.bridge.on('stderr', (line: string) => {
|
|
logger.log('debug', `[rust-bridge] ${line}`);
|
|
});
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Static configuration & singleton
|
|
// -----------------------------------------------------------------------
|
|
|
|
/** Get or create the singleton instance. */
|
|
public static getInstance(): RustSecurityBridge {
|
|
if (!RustSecurityBridge.instance) {
|
|
RustSecurityBridge.instance = new RustSecurityBridge();
|
|
}
|
|
return RustSecurityBridge.instance;
|
|
}
|
|
|
|
/** Reset the singleton instance (for testing). */
|
|
public static resetInstance(): void {
|
|
if (RustSecurityBridge.instance) {
|
|
RustSecurityBridge.instance.stopHealthCheck();
|
|
if (RustSecurityBridge.instance._restartTimer) {
|
|
clearTimeout(RustSecurityBridge.instance._restartTimer);
|
|
RustSecurityBridge.instance._restartTimer = null;
|
|
}
|
|
RustSecurityBridge.instance.removeAllListeners();
|
|
}
|
|
RustSecurityBridge.instance = null;
|
|
}
|
|
|
|
/** Configure resilience parameters. Can be called before or after getInstance(). */
|
|
public static configure(config: Partial<IBridgeResilienceConfig>): void {
|
|
RustSecurityBridge._resilienceConfig = {
|
|
...RustSecurityBridge._resilienceConfig,
|
|
...config,
|
|
};
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// State management
|
|
// -----------------------------------------------------------------------
|
|
|
|
/** Current bridge state. */
|
|
public get state(): BridgeState {
|
|
return this._state;
|
|
}
|
|
|
|
/** Whether the Rust process is currently running and accepting commands. */
|
|
public get running(): boolean {
|
|
return this._running;
|
|
}
|
|
|
|
private setState(newState: BridgeState): void {
|
|
const oldState = this._state;
|
|
if (oldState === newState) return;
|
|
this._state = newState;
|
|
logger.log('info', `Rust bridge state: ${oldState} -> ${newState}`);
|
|
this.emit('stateChange', { oldState, newState });
|
|
}
|
|
|
|
/**
|
|
* Throws a descriptive error if the bridge is not in Running state.
|
|
* Called at the top of every command method.
|
|
*/
|
|
private ensureRunning(): void {
|
|
if (this._state === BridgeState.Running && this._running) {
|
|
return;
|
|
}
|
|
switch (this._state) {
|
|
case BridgeState.Idle:
|
|
throw new Error('Rust bridge has not been started yet. Call start() first.');
|
|
case BridgeState.Starting:
|
|
throw new Error('Rust bridge is still starting. Wait for start() to resolve.');
|
|
case BridgeState.Restarting:
|
|
throw new Error('Rust bridge is restarting after a crash. Commands will resume once it recovers.');
|
|
case BridgeState.Failed:
|
|
throw new Error('Rust bridge has failed after exhausting all restart attempts.');
|
|
case BridgeState.Stopped:
|
|
throw new Error('Rust bridge has been stopped. Call start() to restart it.');
|
|
default:
|
|
throw new Error(`Rust bridge is not running (state=${this._state}).`);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// 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 && this._state === BridgeState.Running) {
|
|
return true;
|
|
}
|
|
|
|
this._deliberateStop = false;
|
|
this._restartAttempts = 0;
|
|
this.setState(BridgeState.Starting);
|
|
|
|
try {
|
|
const ok = await this.bridge.spawn();
|
|
this._running = ok;
|
|
if (ok) {
|
|
this.setState(BridgeState.Running);
|
|
this.startHealthCheck();
|
|
logger.log('info', 'Rust security bridge started');
|
|
} else {
|
|
this.setState(BridgeState.Failed);
|
|
logger.log('warn', 'Rust security bridge failed to start (binary not found or timeout)');
|
|
}
|
|
return ok;
|
|
} catch (err) {
|
|
this.setState(BridgeState.Failed);
|
|
logger.log('error', `Failed to start Rust security bridge: ${(err as Error).message}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/** Kill the Rust process deliberately. */
|
|
public async stop(): Promise<void> {
|
|
this._deliberateStop = true;
|
|
|
|
// Cancel any pending restart
|
|
if (this._restartTimer) {
|
|
clearTimeout(this._restartTimer);
|
|
this._restartTimer = null;
|
|
}
|
|
|
|
this.stopHealthCheck();
|
|
this._smtpServerConfig = null;
|
|
|
|
if (!this._running) {
|
|
this.setState(BridgeState.Stopped);
|
|
return;
|
|
}
|
|
try {
|
|
this.bridge.kill();
|
|
this._running = false;
|
|
this.setState(BridgeState.Stopped);
|
|
logger.log('info', 'Rust security bridge stopped');
|
|
} catch (err) {
|
|
logger.log('error', `Error stopping Rust security bridge: ${(err as Error).message}`);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Auto-restart with exponential backoff
|
|
// -----------------------------------------------------------------------
|
|
|
|
private attemptRestart(): void {
|
|
const config = RustSecurityBridge._resilienceConfig;
|
|
this._restartAttempts++;
|
|
|
|
if (this._restartAttempts > config.maxRestartAttempts) {
|
|
logger.log('error', `Rust bridge exceeded max restart attempts (${config.maxRestartAttempts}). Giving up.`);
|
|
this.setState(BridgeState.Failed);
|
|
return;
|
|
}
|
|
|
|
this.setState(BridgeState.Restarting);
|
|
this.stopHealthCheck();
|
|
|
|
const delay = Math.min(
|
|
config.restartBackoffBaseMs * Math.pow(2, this._restartAttempts - 1),
|
|
config.restartBackoffMaxMs,
|
|
);
|
|
|
|
logger.log('info', `Rust bridge restart attempt ${this._restartAttempts}/${config.maxRestartAttempts} in ${delay}ms`);
|
|
|
|
this._restartTimer = setTimeout(async () => {
|
|
this._restartTimer = null;
|
|
|
|
// Guard: if stop() was called while we were waiting, don't restart
|
|
if (this._deliberateStop) {
|
|
this.setState(BridgeState.Stopped);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const ok = await this.bridge.spawn();
|
|
this._running = ok;
|
|
|
|
if (ok) {
|
|
logger.log('info', 'Rust bridge restarted successfully');
|
|
this._restartAttempts = 0;
|
|
this.setState(BridgeState.Running);
|
|
this.startHealthCheck();
|
|
await this.restoreAfterRestart();
|
|
} else {
|
|
logger.log('warn', 'Rust bridge restart failed (spawn returned false)');
|
|
this.attemptRestart();
|
|
}
|
|
} catch (err) {
|
|
logger.log('error', `Rust bridge restart failed: ${(err as Error).message}`);
|
|
this.attemptRestart();
|
|
}
|
|
}, delay);
|
|
}
|
|
|
|
/**
|
|
* Restore state after a successful restart:
|
|
* - Re-send startSmtpServer command if the SMTP server was running
|
|
*/
|
|
private async restoreAfterRestart(): Promise<void> {
|
|
if (this._smtpServerConfig) {
|
|
try {
|
|
logger.log('info', 'Restoring SMTP server after bridge restart');
|
|
const result = await this.bridge.sendCommand('startSmtpServer', this._smtpServerConfig);
|
|
if (result?.started) {
|
|
logger.log('info', 'SMTP server restored after bridge restart');
|
|
} else {
|
|
logger.log('warn', 'SMTP server failed to restore after bridge restart');
|
|
}
|
|
} catch (err) {
|
|
logger.log('error', `Failed to restore SMTP server after restart: ${(err as Error).message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Health check
|
|
// -----------------------------------------------------------------------
|
|
|
|
private startHealthCheck(): void {
|
|
this.stopHealthCheck();
|
|
const config = RustSecurityBridge._resilienceConfig;
|
|
|
|
this._healthCheckTimer = setInterval(async () => {
|
|
if (this._state !== BridgeState.Running || !this._running) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const pongPromise = this.bridge.sendCommand('ping', {} as any);
|
|
const timeoutPromise = new Promise<never>((_, reject) =>
|
|
setTimeout(() => reject(new Error('Health check timeout')), config.healthCheckTimeoutMs),
|
|
);
|
|
const res = await Promise.race([pongPromise, timeoutPromise]);
|
|
if (!(res as any)?.pong) {
|
|
throw new Error('Health check: unexpected ping response');
|
|
}
|
|
} catch (err) {
|
|
logger.log('warn', `Rust bridge health check failed: ${(err as Error).message}. Killing process to trigger restart.`);
|
|
try {
|
|
this.bridge.kill();
|
|
} catch {
|
|
// Already dead
|
|
}
|
|
// The exit handler will trigger attemptRestart()
|
|
}
|
|
}, config.healthCheckIntervalMs);
|
|
}
|
|
|
|
private stopHealthCheck(): void {
|
|
if (this._healthCheckTimer) {
|
|
clearInterval(this._healthCheckTimer);
|
|
this._healthCheckTimer = null;
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Commands — thin typed wrappers over sendCommand
|
|
// -----------------------------------------------------------------------
|
|
|
|
/** Ping the Rust process. */
|
|
public async ping(): Promise<boolean> {
|
|
this.ensureRunning();
|
|
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> {
|
|
this.ensureRunning();
|
|
return this.bridge.sendCommand('version', {} as any);
|
|
}
|
|
|
|
/** Validate an email address. */
|
|
public async validateEmail(email: string): Promise<IValidationResult> {
|
|
this.ensureRunning();
|
|
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> {
|
|
this.ensureRunning();
|
|
return this.bridge.sendCommand('detectBounce', opts);
|
|
}
|
|
|
|
/** Scan email content for threats (phishing, spam, malware, etc.). */
|
|
public async scanContent(opts: {
|
|
subject?: string;
|
|
textBody?: string;
|
|
htmlBody?: string;
|
|
attachmentNames?: string[];
|
|
}): Promise<IContentScanResult> {
|
|
this.ensureRunning();
|
|
return this.bridge.sendCommand('scanContent', opts);
|
|
}
|
|
|
|
/** Check IP reputation via DNSBL. */
|
|
public async checkIpReputation(ip: string): Promise<IReputationResult> {
|
|
this.ensureRunning();
|
|
return this.bridge.sendCommand('checkIpReputation', { ip });
|
|
}
|
|
|
|
/** Verify DKIM signatures on a raw email message. */
|
|
public async verifyDkim(rawMessage: string): Promise<IDkimVerificationResult[]> {
|
|
this.ensureRunning();
|
|
return this.bridge.sendCommand('verifyDkim', { rawMessage });
|
|
}
|
|
|
|
/** Sign an email with DKIM (RSA or Ed25519). */
|
|
public async signDkim(opts: {
|
|
rawMessage: string;
|
|
domain: string;
|
|
selector?: string;
|
|
privateKey: string;
|
|
keyType?: string;
|
|
}): Promise<{ header: string; signedMessage: string }> {
|
|
this.ensureRunning();
|
|
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> {
|
|
this.ensureRunning();
|
|
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> {
|
|
this.ensureRunning();
|
|
return this.bridge.sendCommand('verifyEmail', opts);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// SMTP Client — outbound email delivery via Rust
|
|
// -----------------------------------------------------------------------
|
|
|
|
/** Send a structured email via the Rust SMTP client. */
|
|
public async sendOutboundEmail(opts: ISmtpSendOptions): Promise<ISmtpSendResult> {
|
|
this.ensureRunning();
|
|
return this.bridge.sendCommand('sendEmail', opts);
|
|
}
|
|
|
|
/** Send a pre-formatted raw email via the Rust SMTP client. */
|
|
public async sendRawEmail(opts: ISmtpSendRawOptions): Promise<ISmtpSendResult> {
|
|
this.ensureRunning();
|
|
return this.bridge.sendCommand('sendRawEmail', opts);
|
|
}
|
|
|
|
/** Verify connectivity to an SMTP server. */
|
|
public async verifySmtpConnection(opts: ISmtpVerifyOptions): Promise<ISmtpVerifyResult> {
|
|
this.ensureRunning();
|
|
return this.bridge.sendCommand('verifySmtpConnection', opts);
|
|
}
|
|
|
|
/** Close a specific connection pool (or all pools if no key is given). */
|
|
public async closeSmtpPool(poolKey?: string): Promise<void> {
|
|
this.ensureRunning();
|
|
await this.bridge.sendCommand('closeSmtpPool', poolKey ? { poolKey } : ({} as any));
|
|
}
|
|
|
|
/** Get status of all SMTP client connection pools. */
|
|
public async getSmtpPoolStatus(): Promise<ISmtpPoolStatus> {
|
|
this.ensureRunning();
|
|
return this.bridge.sendCommand('getSmtpPoolStatus', {} as any);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// SMTP Server lifecycle
|
|
// -----------------------------------------------------------------------
|
|
|
|
/**
|
|
* Start the Rust SMTP server.
|
|
* The server will listen on the configured ports and emit events for
|
|
* emailReceived and authRequest that must be handled by the caller.
|
|
*/
|
|
public async startSmtpServer(config: ISmtpServerConfig): Promise<boolean> {
|
|
this.ensureRunning();
|
|
this._smtpServerConfig = config;
|
|
const result = await this.bridge.sendCommand('startSmtpServer', config);
|
|
return result?.started === true;
|
|
}
|
|
|
|
/** Stop the Rust SMTP server. */
|
|
public async stopSmtpServer(): Promise<void> {
|
|
this.ensureRunning();
|
|
this._smtpServerConfig = null;
|
|
await this.bridge.sendCommand('stopSmtpServer', {} as any);
|
|
}
|
|
|
|
/**
|
|
* Send the result of email processing back to the Rust SMTP server.
|
|
* This resolves a pending correlation-ID callback, allowing the Rust
|
|
* server to send the SMTP response to the client.
|
|
*/
|
|
public async sendEmailProcessingResult(opts: {
|
|
correlationId: string;
|
|
accepted: boolean;
|
|
smtpCode?: number;
|
|
smtpMessage?: string;
|
|
}): Promise<void> {
|
|
this.ensureRunning();
|
|
await this.bridge.sendCommand('emailProcessingResult', opts);
|
|
}
|
|
|
|
/**
|
|
* Send the result of authentication validation back to the Rust SMTP server.
|
|
*/
|
|
public async sendAuthResult(opts: {
|
|
correlationId: string;
|
|
success: boolean;
|
|
message?: string;
|
|
}): Promise<void> {
|
|
this.ensureRunning();
|
|
await this.bridge.sendCommand('authResult', opts);
|
|
}
|
|
|
|
/**
|
|
* Send SCRAM credentials back to the Rust SMTP server.
|
|
* Values (salt, storedKey, serverKey) must be base64-encoded.
|
|
*/
|
|
public async sendScramCredentialResult(opts: {
|
|
correlationId: string;
|
|
found: boolean;
|
|
salt?: string;
|
|
iterations?: number;
|
|
storedKey?: string;
|
|
serverKey?: string;
|
|
}): Promise<void> {
|
|
this.ensureRunning();
|
|
await this.bridge.sendCommand('scramCredentialResult', opts);
|
|
}
|
|
|
|
/** Update rate limit configuration at runtime. */
|
|
public async configureRateLimits(config: IRateLimitConfig): Promise<void> {
|
|
this.ensureRunning();
|
|
await this.bridge.sendCommand('configureRateLimits', config);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Event registration — delegates to the underlying bridge EventEmitter
|
|
// -----------------------------------------------------------------------
|
|
|
|
/**
|
|
* Register a handler for emailReceived events from the Rust SMTP server.
|
|
* These events fire when a complete email has been received and needs processing.
|
|
*/
|
|
public onEmailReceived(handler: (data: IEmailReceivedEvent) => void): void {
|
|
this.bridge.on('management:emailReceived', handler);
|
|
}
|
|
|
|
/**
|
|
* Register a handler for authRequest events from the Rust SMTP server.
|
|
* The handler must call sendAuthResult() with the correlationId.
|
|
*/
|
|
public onAuthRequest(handler: (data: IAuthRequestEvent) => void): void {
|
|
this.bridge.on('management:authRequest', handler);
|
|
}
|
|
|
|
/**
|
|
* Register a handler for scramCredentialRequest events from the Rust SMTP server.
|
|
* The handler must call sendScramCredentialResult() with the correlationId.
|
|
*/
|
|
public onScramCredentialRequest(handler: (data: IScramCredentialRequestEvent) => void): void {
|
|
this.bridge.on('management:scramCredentialRequest', handler);
|
|
}
|
|
|
|
/** Remove an emailReceived event handler. */
|
|
public offEmailReceived(handler: (data: IEmailReceivedEvent) => void): void {
|
|
this.bridge.off('management:emailReceived', handler);
|
|
}
|
|
|
|
/** Remove an authRequest event handler. */
|
|
public offAuthRequest(handler: (data: IAuthRequestEvent) => void): void {
|
|
this.bridge.off('management:authRequest', handler);
|
|
}
|
|
|
|
/** Remove a scramCredentialRequest event handler. */
|
|
public offScramCredentialRequest(handler: (data: IScramCredentialRequestEvent) => void): void {
|
|
this.bridge.off('management:scramCredentialRequest', handler);
|
|
}
|
|
}
|
|
|
|
// Re-export interfaces for consumers
|
|
export type {
|
|
IDkimVerificationResult,
|
|
ISpfResult,
|
|
IDmarcResult,
|
|
IEmailSecurityResult,
|
|
IValidationResult,
|
|
IBounceDetection,
|
|
IContentScanResult,
|
|
IReputationResult as IRustReputationResult,
|
|
IVersionInfo,
|
|
ISmtpServerConfig,
|
|
IRateLimitConfig,
|
|
IEmailData,
|
|
IEmailReceivedEvent,
|
|
IAuthRequestEvent,
|
|
IScramCredentialRequestEvent,
|
|
IOutboundEmail,
|
|
ISmtpSendResult,
|
|
ISmtpSendOptions,
|
|
ISmtpSendRawOptions,
|
|
ISmtpVerifyOptions,
|
|
ISmtpVerifyResult,
|
|
ISmtpPoolStatus,
|
|
};
|