2026-02-10 15:31:31 +00:00
import * as plugins from '../../plugins.js' ;
import * as paths from '../../paths.js' ;
2025-10-28 19:46:17 +00:00
import { EventEmitter } from 'events' ;
2026-02-10 15:31:31 +00:00
import { logger } from '../../logger.js' ;
2025-10-24 08:09:29 +00:00
import {
SecurityLogger ,
SecurityLogLevel ,
SecurityEventType
2026-02-10 15:31:31 +00:00
} from '../../security/index.js' ;
import { DKIMCreator } from '../security/classes.dkimcreator.js' ;
import { IPReputationChecker } from '../../security/classes.ipreputationchecker.js' ;
2026-02-10 16:25:55 +00:00
import { RustSecurityBridge } from '../../security/classes.rustsecuritybridge.js' ;
2026-02-10 22:00:44 +00:00
import type { IEmailReceivedEvent , IAuthRequestEvent , IEmailData } from '../../security/classes.rustsecuritybridge.js' ;
2026-02-10 15:31:31 +00:00
import { EmailRouter } from './classes.email.router.js' ;
import type { IEmailRoute , IEmailAction , IEmailContext , IEmailDomainConfig } from './interfaces.js' ;
import { Email } from '../core/classes.email.js' ;
import { DomainRegistry } from './classes.domain.registry.js' ;
import { DnsManager } from './classes.dns.manager.js' ;
import { BounceManager , BounceType , BounceCategory } from '../core/classes.bouncemanager.js' ;
import { createPooledSmtpClient } from '../delivery/smtpclient/create-client.js' ;
import type { SmtpClient } from '../delivery/smtpclient/smtp-client.js' ;
import { MultiModeDeliverySystem , type IMultiModeDeliveryOptions } from '../delivery/classes.delivery.system.js' ;
import { UnifiedDeliveryQueue , type IQueueOptions } from '../delivery/classes.delivery.queue.js' ;
import { UnifiedRateLimiter , type IHierarchicalRateLimits } from '../delivery/classes.unified.rate.limiter.js' ;
import { SmtpState } from '../delivery/interfaces.js' ;
import type { EmailProcessingMode , ISmtpSession as IBaseSmtpSession } from '../delivery/interfaces.js' ;
/** External DcRouter interface shape used by UnifiedEmailServer */
interface DcRouter {
storageManager : any ;
dnsServer? : any ;
options? : any ;
}
2025-10-24 08:09:29 +00:00
/ * *
* Extended SMTP session interface with route information
* /
export interface IExtendedSmtpSession extends ISmtpSession {
/ * *
* Matched route for this session
* /
matchedRoute? : IEmailRoute ;
}
/ * *
* Options for the unified email server
* /
export interface IUnifiedEmailServerOptions {
// Base server options
ports : number [ ] ;
hostname : string ;
domains : IEmailDomainConfig [ ] ; // Domain configurations
banner? : string ;
debug? : boolean ;
useSocketHandler? : boolean ; // Use socket-handler mode instead of port listening
// Authentication options
auth ? : {
required? : boolean ;
methods ? : ( 'PLAIN' | 'LOGIN' | 'OAUTH2' ) [ ] ;
users? : Array < { username : string , password : string } > ;
} ;
// TLS options
tls ? : {
certPath? : string ;
keyPath? : string ;
caPath? : string ;
minVersion? : string ;
ciphers? : string ;
} ;
// Limits
maxMessageSize? : number ;
maxClients? : number ;
maxConnections? : number ;
// Connection options
connectionTimeout? : number ;
socketTimeout? : number ;
// Email routing rules
routes : IEmailRoute [ ] ;
// Global defaults for all domains
defaults ? : {
dnsMode ? : 'forward' | 'internal-dns' | 'external-dns' ;
dkim? : IEmailDomainConfig [ 'dkim' ] ;
rateLimits? : IEmailDomainConfig [ 'rateLimits' ] ;
} ;
// Outbound settings
outbound ? : {
maxConnections? : number ;
connectionTimeout? : number ;
socketTimeout? : number ;
retryAttempts? : number ;
defaultFrom? : string ;
} ;
// Rate limiting (global limits, can be overridden per domain)
rateLimits? : IHierarchicalRateLimits ;
}
/ * *
* Extended SMTP session interface for UnifiedEmailServer
* /
export interface ISmtpSession extends IBaseSmtpSession {
/ * *
* User information if authenticated
* /
user ? : {
username : string ;
[ key : string ] : any ;
} ;
/ * *
* Matched route for this session
* /
matchedRoute? : IEmailRoute ;
}
/ * *
* Authentication data for SMTP
* /
2026-02-10 15:31:31 +00:00
import type { ISmtpAuth } from '../delivery/interfaces.js' ;
2025-10-24 08:09:29 +00:00
export type IAuthData = ISmtpAuth ;
/ * *
* Server statistics
* /
export interface IServerStats {
startTime : Date ;
connections : {
current : number ;
total : number ;
} ;
messages : {
processed : number ;
delivered : number ;
failed : number ;
} ;
processingTime : {
avg : number ;
max : number ;
min : number ;
} ;
}
/ * *
* Unified email server that handles all email traffic with pattern - based routing
* /
2025-10-28 19:46:17 +00:00
export class UnifiedEmailServer extends EventEmitter {
2025-10-24 08:09:29 +00:00
private dcRouter : DcRouter ;
private options : IUnifiedEmailServerOptions ;
private emailRouter : EmailRouter ;
public domainRegistry : DomainRegistry ;
private servers : any [ ] = [ ] ;
private stats : IServerStats ;
// Add components needed for sending and securing emails
public dkimCreator : DKIMCreator ;
2026-02-10 16:25:55 +00:00
private rustBridge : RustSecurityBridge ;
private ipReputationChecker : IPReputationChecker ;
2025-10-24 08:09:29 +00:00
private bounceManager : BounceManager ;
public deliveryQueue : UnifiedDeliveryQueue ;
public deliverySystem : MultiModeDeliverySystem ;
private rateLimiter : UnifiedRateLimiter ; // TODO: Implement rate limiting in SMTP server handlers
private dkimKeys : Map < string , string > = new Map ( ) ; // domain -> private key
private smtpClients : Map < string , SmtpClient > = new Map ( ) ; // host:port -> client
constructor ( dcRouter : DcRouter , options : IUnifiedEmailServerOptions ) {
super ( ) ;
this . dcRouter = dcRouter ;
// Set default options
this . options = {
. . . options ,
banner : options.banner || ` ${ options . hostname } ESMTP UnifiedEmailServer ` ,
maxMessageSize : options.maxMessageSize || 10 * 1024 * 1024 , // 10MB
maxClients : options.maxClients || 100 ,
maxConnections : options.maxConnections || 1000 ,
connectionTimeout : options.connectionTimeout || 60000 , // 1 minute
socketTimeout : options.socketTimeout || 60000 // 1 minute
} ;
2026-02-10 16:25:55 +00:00
// Initialize Rust security bridge (singleton)
this . rustBridge = RustSecurityBridge . getInstance ( ) ;
2025-10-24 08:09:29 +00:00
// Initialize DKIM creator with storage manager
this . dkimCreator = new DKIMCreator ( paths . keysDir , dcRouter . storageManager ) ;
// Initialize IP reputation checker with storage manager
this . ipReputationChecker = IPReputationChecker . getInstance ( {
enableLocalCache : true ,
enableDNSBL : true ,
enableIPInfo : true
} , dcRouter . storageManager ) ;
// Initialize bounce manager with storage manager
this . bounceManager = new BounceManager ( {
maxCacheSize : 10000 ,
cacheTTL : 30 * 24 * 60 * 60 * 1000 , // 30 days
storageManager : dcRouter.storageManager
} ) ;
// Initialize domain registry
this . domainRegistry = new DomainRegistry ( options . domains , options . defaults ) ;
// Initialize email router with routes and storage manager
this . emailRouter = new EmailRouter ( options . routes || [ ] , {
storageManager : dcRouter.storageManager ,
persistChanges : true
} ) ;
// Initialize rate limiter
this . rateLimiter = new UnifiedRateLimiter ( options . rateLimits || {
global : {
maxConnectionsPerIP : 10 ,
maxMessagesPerMinute : 100 ,
maxRecipientsPerMessage : 50 ,
maxErrorsPerIP : 10 ,
maxAuthFailuresPerIP : 5 ,
blockDuration : 300000 // 5 minutes
}
} ) ;
// Initialize delivery components
const queueOptions : IQueueOptions = {
storageType : 'memory' , // Default to memory storage
maxRetries : 3 ,
baseRetryDelay : 300000 , // 5 minutes
maxRetryDelay : 3600000 // 1 hour
} ;
this . deliveryQueue = new UnifiedDeliveryQueue ( queueOptions ) ;
const deliveryOptions : IMultiModeDeliveryOptions = {
globalRateLimit : 100 , // Default to 100 emails per minute
concurrentDeliveries : 10 ,
processBounces : true ,
bounceHandler : {
processSmtpFailure : this.processSmtpFailure.bind ( this )
} ,
onDeliverySuccess : async ( item , _result ) = > {
// Record delivery success event for reputation monitoring
const email = item . processingResult as Email ;
const senderDomain = email . from . split ( '@' ) [ 1 ] ;
if ( senderDomain ) {
this . recordReputationEvent ( senderDomain , {
type : 'delivered' ,
count : email.to.length
} ) ;
}
}
} ;
this . deliverySystem = new MultiModeDeliverySystem ( this . deliveryQueue , deliveryOptions , this ) ;
// Initialize statistics
this . stats = {
startTime : new Date ( ) ,
connections : {
current : 0 ,
total : 0
} ,
messages : {
processed : 0 ,
delivered : 0 ,
failed : 0
} ,
processingTime : {
avg : 0 ,
max : 0 ,
min : 0
}
} ;
// We'll create the SMTP servers during the start() method
}
/ * *
* Get or create an SMTP client for the given host and port
* Uses connection pooling for efficiency
* /
public getSmtpClient ( host : string , port : number = 25 ) : SmtpClient {
const clientKey = ` ${ host } : ${ port } ` ;
// Check if we already have a client for this destination
let client = this . smtpClients . get ( clientKey ) ;
if ( ! client ) {
// Create a new pooled SMTP client
client = createPooledSmtpClient ( {
host ,
port ,
secure : port === 465 ,
connectionTimeout : this.options.outbound?.connectionTimeout || 30000 ,
socketTimeout : this.options.outbound?.socketTimeout || 120000 ,
maxConnections : this.options.outbound?.maxConnections || 10 ,
maxMessages : 1000 , // Messages per connection before reconnect
pool : true ,
debug : false
} ) ;
this . smtpClients . set ( clientKey , client ) ;
logger . log ( 'info' , ` Created new SMTP client pool for ${ clientKey } ` ) ;
}
return client ;
}
/ * *
* Start the unified email server
* /
public async start ( ) : Promise < void > {
logger . log ( 'info' , ` Starting UnifiedEmailServer on ports: ${ ( this . options . ports as number [ ] ) . join ( ', ' ) } ` ) ;
try {
// Initialize the delivery queue
await this . deliveryQueue . initialize ( ) ;
logger . log ( 'info' , 'Email delivery queue initialized' ) ;
// Start the delivery system
await this . deliverySystem . start ( ) ;
logger . log ( 'info' , 'Email delivery system started' ) ;
2026-02-10 16:25:55 +00:00
2026-02-10 20:30:43 +00:00
// Start Rust security bridge — required for all security operations
2026-02-10 16:25:55 +00:00
const bridgeOk = await this . rustBridge . start ( ) ;
2026-02-10 20:30:43 +00:00
if ( ! bridgeOk ) {
throw new Error ( 'Rust security bridge failed to start. The mailer-bin binary is required. Run "pnpm build" to compile it.' ) ;
2026-02-10 16:25:55 +00:00
}
2026-02-10 20:30:43 +00:00
logger . log ( 'info' , 'Rust security bridge started — Rust is the primary security backend' ) ;
2026-02-10 16:25:55 +00:00
2026-02-10 23:23:00 +00:00
// Listen for bridge state changes to propagate resilience events
this . rustBridge . on ( 'stateChange' , ( { oldState , newState } : { oldState : string ; newState : string } ) = > {
if ( newState === 'failed' ) this . emit ( 'bridgeFailed' ) ;
else if ( newState === 'restarting' ) this . emit ( 'bridgeRestarting' ) ;
else if ( newState === 'running' && oldState === 'restarting' ) this . emit ( 'bridgeRecovered' ) ;
} ) ;
2025-10-24 08:09:29 +00:00
// Set up DKIM for all domains
await this . setupDkimForDomains ( ) ;
logger . log ( 'info' , 'DKIM configuration completed for all domains' ) ;
// Create DNS manager and ensure all DNS records are created
const dnsManager = new DnsManager ( this . dcRouter ) ;
await dnsManager . ensureDnsRecords ( this . domainRegistry . getAllConfigs ( ) , this . dkimCreator ) ;
logger . log ( 'info' , 'DNS records ensured for all configured domains' ) ;
// Apply per-domain rate limits
this . applyDomainRateLimits ( ) ;
logger . log ( 'info' , 'Per-domain rate limits configured' ) ;
// Check and rotate DKIM keys if needed
await this . checkAndRotateDkimKeys ( ) ;
logger . log ( 'info' , 'DKIM key rotation check completed' ) ;
// Ensure we have the necessary TLS options
const hasTlsConfig = this . options . tls ? . keyPath && this . options . tls ? . certPath ;
2026-02-10 22:00:44 +00:00
2025-10-24 08:09:29 +00:00
// Prepare the certificate and key if available
2026-02-10 22:00:44 +00:00
let tlsCertPem : string | undefined ;
let tlsKeyPem : string | undefined ;
2025-10-24 08:09:29 +00:00
if ( hasTlsConfig ) {
try {
2026-02-10 22:00:44 +00:00
tlsKeyPem = plugins . fs . readFileSync ( this . options . tls . keyPath ! , 'utf8' ) ;
tlsCertPem = plugins . fs . readFileSync ( this . options . tls . certPath ! , 'utf8' ) ;
2025-10-24 08:09:29 +00:00
logger . log ( 'info' , 'TLS certificates loaded successfully' ) ;
} catch ( error ) {
logger . log ( 'warn' , ` Failed to load TLS certificates: ${ error . message } ` ) ;
}
}
2026-02-10 22:00:44 +00:00
// --- Start Rust SMTP server ---
// Register event handlers for email reception and auth
this . rustBridge . onEmailReceived ( async ( data ) = > {
try {
await this . handleRustEmailReceived ( data ) ;
} catch ( err ) {
logger . log ( 'error' , ` Error handling email from Rust SMTP: ${ ( err as Error ) . message } ` ) ;
2026-02-10 23:23:00 +00:00
// Send rejection back to Rust (may fail if bridge is restarting)
try {
await this . rustBridge . sendEmailProcessingResult ( {
correlationId : data.correlationId ,
accepted : false ,
smtpCode : 451 ,
smtpMessage : 'Internal processing error' ,
} ) ;
} catch ( sendErr ) {
logger . log ( 'warn' , ` Could not send rejection back to Rust: ${ ( sendErr as Error ) . message } ` ) ;
}
2026-02-10 22:00:44 +00:00
}
} ) ;
this . rustBridge . onAuthRequest ( async ( data ) = > {
try {
await this . handleRustAuthRequest ( data ) ;
} catch ( err ) {
logger . log ( 'error' , ` Error handling auth from Rust SMTP: ${ ( err as Error ) . message } ` ) ;
2026-02-10 23:23:00 +00:00
try {
await this . rustBridge . sendAuthResult ( {
correlationId : data.correlationId ,
success : false ,
message : 'Internal auth error' ,
} ) ;
} catch ( sendErr ) {
logger . log ( 'warn' , ` Could not send auth rejection back to Rust: ${ ( sendErr as Error ) . message } ` ) ;
}
2026-02-10 22:00:44 +00:00
}
} ) ;
// Determine which ports need STARTTLS and which need implicit TLS
const smtpPorts = ( this . options . ports as number [ ] ) . filter ( p = > p !== 465 ) ;
const securePort = ( this . options . ports as number [ ] ) . find ( p = > p === 465 ) ;
const started = await this . rustBridge . startSmtpServer ( {
hostname : this.options.hostname ,
ports : smtpPorts ,
securePort : securePort ,
tlsCertPem ,
tlsKeyPem ,
maxMessageSize : this.options.maxMessageSize || 10 * 1024 * 1024 ,
maxConnections : this.options.maxConnections || this . options . maxClients || 100 ,
maxRecipients : 100 ,
connectionTimeoutSecs : this.options.connectionTimeout ? Math . floor ( this . options . connectionTimeout / 1000 ) : 30 ,
dataTimeoutSecs : 60 ,
authEnabled : ! ! this . options . auth ? . required || ! ! ( this . options . auth ? . users ? . length ) ,
maxAuthFailures : 3 ,
socketTimeoutSecs : this.options.socketTimeout ? Math . floor ( this . options . socketTimeout / 1000 ) : 300 ,
processingTimeoutSecs : 30 ,
rateLimits : this.options.rateLimits ? {
maxConnectionsPerIp : this.options.rateLimits.global?.maxConnectionsPerIP || 50 ,
maxMessagesPerSender : this.options.rateLimits.global?.maxMessagesPerMinute || 100 ,
maxAuthFailuresPerIp : this.options.rateLimits.global?.maxAuthFailuresPerIP || 5 ,
windowSecs : 60 ,
} : undefined ,
} ) ;
if ( ! started ) {
throw new Error ( 'Failed to start Rust SMTP server' ) ;
2025-10-24 08:09:29 +00:00
}
2026-02-10 22:00:44 +00:00
logger . log ( 'info' , ` Rust SMTP server listening on ports: ${ smtpPorts . join ( ', ' ) } ${ securePort ? ` + ${ securePort } (TLS) ` : '' } ` ) ;
2025-10-24 08:09:29 +00:00
logger . log ( 'info' , 'UnifiedEmailServer started successfully' ) ;
this . emit ( 'started' ) ;
} catch ( error ) {
logger . log ( 'error' , ` Failed to start UnifiedEmailServer: ${ error . message } ` ) ;
throw error ;
}
}
/ * *
* Stop the unified email server
* /
public async stop ( ) : Promise < void > {
logger . log ( 'info' , 'Stopping UnifiedEmailServer' ) ;
try {
2026-02-10 22:00:44 +00:00
// Stop the Rust SMTP server first
try {
await this . rustBridge . stopSmtpServer ( ) ;
logger . log ( 'info' , 'Rust SMTP server stopped' ) ;
} catch ( err ) {
logger . log ( 'warn' , ` Error stopping Rust SMTP server: ${ ( err as Error ) . message } ` ) ;
}
2025-10-24 08:09:29 +00:00
// Clear the servers array - servers will be garbage collected
this . servers = [ ] ;
2026-02-10 16:25:55 +00:00
2026-02-10 23:23:00 +00:00
// Remove bridge state change listener and stop bridge
this . rustBridge . removeAllListeners ( 'stateChange' ) ;
2026-02-10 16:25:55 +00:00
await this . rustBridge . stop ( ) ;
2025-10-24 08:09:29 +00:00
// Stop the delivery system
if ( this . deliverySystem ) {
await this . deliverySystem . stop ( ) ;
logger . log ( 'info' , 'Email delivery system stopped' ) ;
}
// Shut down the delivery queue
if ( this . deliveryQueue ) {
await this . deliveryQueue . shutdown ( ) ;
logger . log ( 'info' , 'Email delivery queue shut down' ) ;
}
// Close all SMTP client connections
for ( const [ clientKey , client ] of this . smtpClients ) {
try {
await client . close ( ) ;
logger . log ( 'info' , ` Closed SMTP client pool for ${ clientKey } ` ) ;
} catch ( error ) {
logger . log ( 'warn' , ` Error closing SMTP client for ${ clientKey } : ${ error . message } ` ) ;
}
}
this . smtpClients . clear ( ) ;
logger . log ( 'info' , 'UnifiedEmailServer stopped successfully' ) ;
this . emit ( 'stopped' ) ;
} catch ( error ) {
logger . log ( 'error' , ` Error stopping UnifiedEmailServer: ${ error . message } ` ) ;
throw error ;
}
}
2026-02-10 22:00:44 +00:00
// -----------------------------------------------------------------------
// Rust SMTP server event handlers
// -----------------------------------------------------------------------
/ * *
* Handle an emailReceived event from the Rust SMTP server .
* Decodes the email data , processes it through the routing system ,
* and sends back the result via the correlation - ID callback .
* /
private async handleRustEmailReceived ( data : IEmailReceivedEvent ) : Promise < void > {
const { correlationId , mailFrom , rcptTo , remoteAddr , clientHostname , secure , authenticatedUser } = data ;
logger . log ( 'info' , ` Rust SMTP received email from= ${ mailFrom } to= ${ rcptTo . join ( ',' ) } remote= ${ remoteAddr } ` ) ;
try {
// Decode the email data
let rawMessageBuffer : Buffer ;
if ( data . data . type === 'inline' && data . data . base64 ) {
rawMessageBuffer = Buffer . from ( data . data . base64 , 'base64' ) ;
} else if ( data . data . type === 'file' && data . data . path ) {
rawMessageBuffer = plugins . fs . readFileSync ( data . data . path ) ;
// Clean up temp file
try {
plugins . fs . unlinkSync ( data . data . path ) ;
} catch {
// Ignore cleanup errors
}
} else {
throw new Error ( 'Invalid email data transport' ) ;
}
// Build a session-like object for processEmailByMode
const session : IExtendedSmtpSession = {
id : data.sessionId || 'rust-' + Math . random ( ) . toString ( 36 ) . substring ( 2 ) ,
state : SmtpState.FINISHED ,
mailFrom : mailFrom ,
rcptTo : rcptTo ,
emailData : rawMessageBuffer.toString ( 'utf8' ) ,
useTLS : secure ,
connectionEnded : false ,
remoteAddress : remoteAddr ,
clientHostname : clientHostname || '' ,
secure : secure ,
authenticated : ! ! authenticatedUser ,
envelope : {
mailFrom : { address : mailFrom , args : { } } ,
rcptTo : rcptTo.map ( addr = > ( { address : addr , args : { } } ) ) ,
} ,
} ;
if ( authenticatedUser ) {
session . user = { username : authenticatedUser } ;
}
2026-02-10 22:26:20 +00:00
// Attach pre-computed security results from Rust in-process pipeline
if ( data . securityResults ) {
( session as any ) . _precomputedSecurityResults = data . securityResults ;
}
2026-02-10 22:00:44 +00:00
// Process the email through the routing system
await this . processEmailByMode ( rawMessageBuffer , session ) ;
// Send acceptance back to Rust
await this . rustBridge . sendEmailProcessingResult ( {
correlationId ,
accepted : true ,
smtpCode : 250 ,
smtpMessage : '2.0.0 Message accepted for delivery' ,
} ) ;
} catch ( err ) {
logger . log ( 'error' , ` Failed to process email from Rust SMTP: ${ ( err as Error ) . message } ` ) ;
await this . rustBridge . sendEmailProcessingResult ( {
correlationId ,
accepted : false ,
smtpCode : 550 ,
smtpMessage : ` 5.0.0 Processing failed: ${ ( err as Error ) . message } ` ,
} ) ;
}
}
/ * *
* Handle an authRequest event from the Rust SMTP server .
* Validates credentials and sends back the result .
* /
private async handleRustAuthRequest ( data : IAuthRequestEvent ) : Promise < void > {
const { correlationId , username , password , remoteAddr } = data ;
logger . log ( 'info' , ` Rust SMTP auth request for user= ${ username } from= ${ remoteAddr } ` ) ;
// Check against configured users
const users = this . options . auth ? . users || [ ] ;
const matched = users . find (
u = > u . username === username && u . password === password
) ;
if ( matched ) {
await this . rustBridge . sendAuthResult ( {
correlationId ,
success : true ,
} ) ;
} else {
logger . log ( 'warn' , ` Auth failed for user= ${ username } from= ${ remoteAddr } ` ) ;
await this . rustBridge . sendAuthResult ( {
correlationId ,
success : false ,
message : 'Invalid credentials' ,
} ) ;
}
}
2026-02-10 16:25:55 +00:00
/ * *
2026-02-10 22:26:20 +00:00
* Verify inbound email security ( DKIM / SPF / DMARC ) using pre - computed Rust results
* or falling back to IPC call if no pre - computed results are available .
2026-02-10 16:25:55 +00:00
* /
private async verifyInboundSecurity ( email : Email , session : IExtendedSmtpSession ) : Promise < void > {
try {
2026-02-10 22:26:20 +00:00
// Check for pre-computed results from Rust in-process security pipeline
const precomputed = ( session as any ) . _precomputedSecurityResults ;
let result : any ;
if ( precomputed ) {
logger . log ( 'info' , 'Using pre-computed security results from Rust in-process pipeline' ) ;
result = precomputed ;
} else {
2026-02-10 23:23:00 +00:00
// Fallback: IPC round-trip to Rust (for backward compat)
2026-02-10 22:26:20 +00:00
const rawMessage = session . emailData || email . toRFC822String ( ) ;
result = await this . rustBridge . verifyEmail ( {
rawMessage ,
ip : session.remoteAddress ,
heloDomain : session.clientHostname || '' ,
hostname : this.options.hostname ,
mailFrom : session.envelope?.mailFrom?.address || session . mailFrom || '' ,
} ) ;
}
2026-02-10 16:25:55 +00:00
// Apply DKIM result headers
if ( result . dkim && result . dkim . length > 0 ) {
const dkimSummary = result . dkim
2026-02-10 22:26:20 +00:00
. map ( ( d : any ) = > ` ${ d . status } ${ d . domain ? ` ( ${ d . domain } ) ` : '' } ` )
2026-02-10 16:25:55 +00:00
. join ( ', ' ) ;
2026-02-10 16:38:31 +00:00
email . addHeader ( 'X-DKIM-Result' , dkimSummary ) ;
2026-02-10 16:25:55 +00:00
}
// Apply SPF result header
if ( result . spf ) {
2026-02-10 16:38:31 +00:00
email . addHeader ( 'Received-SPF' , ` ${ result . spf . result } (domain: ${ result . spf . domain } , ip: ${ result . spf . ip } ) ` ) ;
2026-02-10 16:25:55 +00:00
// 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 ) {
2026-02-10 16:38:31 +00:00
email . addHeader ( 'X-DMARC-Result' , ` ${ result . dmarc . action } (policy= ${ result . dmarc . policy } , dkim= ${ result . dmarc . dkim_result } , spf= ${ result . dmarc . spf_result } ) ` ) ;
2026-02-10 16:25:55 +00:00
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 ` ) ;
}
}
2026-02-10 22:26:20 +00:00
// Apply content scan results (from pre-computed pipeline)
if ( result . contentScan ) {
const scan = result . contentScan ;
if ( scan . threatScore > 0 ) {
email . addHeader ( 'X-Spam-Score' , String ( scan . threatScore ) ) ;
if ( scan . threatType ) {
email . addHeader ( 'X-Spam-Type' , scan . threatType ) ;
}
if ( scan . threatScore >= 50 ) {
email . mightBeSpam = true ;
logger . log ( 'warn' , ` Content scan threat score ${ scan . threatScore } ( ${ scan . threatType } ) — marking as potential spam ` ) ;
}
}
}
// Apply IP reputation results (from pre-computed pipeline)
if ( result . ipReputation ) {
const rep = result . ipReputation ;
email . addHeader ( 'X-IP-Reputation-Score' , String ( rep . score ) ) ;
if ( rep . is_spam ) {
email . mightBeSpam = true ;
logger . log ( 'warn' , ` IP ${ rep . ip } flagged by reputation check (score= ${ rep . score } ) — marking as potential spam ` ) ;
}
}
2026-02-10 16:25:55 +00:00
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 ` ) ;
}
}
2025-10-24 08:09:29 +00:00
/ * *
* Process email based on routing rules
* /
public async processEmailByMode ( emailData : Email | Buffer , session : IExtendedSmtpSession ) : Promise < Email > {
// Convert Buffer to Email if needed
let email : Email ;
if ( Buffer . isBuffer ( emailData ) ) {
// Parse the email data buffer into an Email object
try {
const parsed = await plugins . mailparser . simpleParser ( emailData ) ;
email = new Email ( {
from : parsed . from ? . value [ 0 ] ? . address || session . envelope . mailFrom . address ,
to : session.envelope.rcptTo [ 0 ] ? . address || '' ,
subject : parsed.subject || '' ,
text : parsed.text || '' ,
html : parsed.html || undefined ,
attachments : parsed.attachments?.map ( att = > ( {
filename : att.filename || '' ,
content : att.content ,
contentType : att.contentType
} ) ) || [ ]
} ) ;
} catch ( error ) {
logger . log ( 'error' , ` Error parsing email data: ${ error . message } ` ) ;
throw new Error ( ` Error parsing email data: ${ error . message } ` ) ;
}
} else {
email = emailData ;
}
2026-02-10 16:25:55 +00:00
// Run inbound security verification (DKIM/SPF/DMARC) via Rust bridge
if ( session . remoteAddress && session . remoteAddress !== '127.0.0.1' ) {
await this . verifyInboundSecurity ( email , session ) ;
}
2025-10-24 08:09:29 +00:00
// First check if this is a bounce notification email
// Look for common bounce notification subject patterns
const subject = email . subject || '' ;
const isBounceLike = /mail delivery|delivery (failed|status|notification)|failure notice|returned mail|undeliverable|delivery problem/i . test ( subject ) ;
if ( isBounceLike ) {
logger . log ( 'info' , ` Email subject matches bounce notification pattern: " ${ subject } " ` ) ;
// Try to process as a bounce
const isBounce = await this . processBounceNotification ( email ) ;
if ( isBounce ) {
logger . log ( 'info' , 'Successfully processed as bounce notification, skipping regular processing' ) ;
return email ;
}
logger . log ( 'info' , 'Not a valid bounce notification, continuing with regular processing' ) ;
}
// Find matching route
const context : IEmailContext = { email , session } ;
const route = await this . emailRouter . evaluateRoutes ( context ) ;
if ( ! route ) {
// No matching route - reject
throw new Error ( 'No matching route for email' ) ;
}
// Store matched route in session
session . matchedRoute = route ;
// Execute action based on route
await this . executeAction ( route . action , email , context ) ;
// Return the processed email
return email ;
}
/ * *
* Execute action based on route configuration
* /
private async executeAction ( action : IEmailAction , email : Email , context : IEmailContext ) : Promise < void > {
switch ( action . type ) {
case 'forward' :
await this . handleForwardAction ( action , email , context ) ;
break ;
case 'process' :
await this . handleProcessAction ( action , email , context ) ;
break ;
case 'deliver' :
await this . handleDeliverAction ( action , email , context ) ;
break ;
case 'reject' :
await this . handleRejectAction ( action , email , context ) ;
break ;
default :
throw new Error ( ` Unknown action type: ${ ( action as any ) . type } ` ) ;
}
}
/ * *
* Handle forward action
* /
private async handleForwardAction ( _action : IEmailAction , email : Email , context : IEmailContext ) : Promise < void > {
if ( ! _action . forward ) {
throw new Error ( 'Forward action requires forward configuration' ) ;
}
const { host , port = 25 , auth , addHeaders } = _action . forward ;
logger . log ( 'info' , ` Forwarding email to ${ host } : ${ port } ` ) ;
// Add forwarding headers
if ( addHeaders ) {
for ( const [ key , value ] of Object . entries ( addHeaders ) ) {
email . headers [ key ] = value ;
}
}
// Add standard forwarding headers
email . headers [ 'X-Forwarded-For' ] = context . session . remoteAddress || 'unknown' ;
email . headers [ 'X-Forwarded-To' ] = email . to . join ( ', ' ) ;
email . headers [ 'X-Forwarded-Date' ] = new Date ( ) . toISOString ( ) ;
// Get SMTP client
const client = this . getSmtpClient ( host , port ) ;
try {
// Send email
await client . sendMail ( email ) ;
logger . log ( 'info' , ` Successfully forwarded email to ${ host } : ${ port } ` ) ;
SecurityLogger . getInstance ( ) . logEvent ( {
level : SecurityLogLevel.INFO ,
type : SecurityEventType . EMAIL_FORWARDING ,
message : 'Email forwarded successfully' ,
ipAddress : context.session.remoteAddress ,
details : {
sessionId : context.session.id ,
routeName : context.session.matchedRoute?.name ,
targetHost : host ,
targetPort : port ,
recipients : email.to
} ,
success : true
} ) ;
} catch ( error ) {
logger . log ( 'error' , ` Failed to forward email: ${ error . message } ` ) ;
SecurityLogger . getInstance ( ) . logEvent ( {
level : SecurityLogLevel.ERROR ,
type : SecurityEventType . EMAIL_FORWARDING ,
message : 'Email forwarding failed' ,
ipAddress : context.session.remoteAddress ,
details : {
sessionId : context.session.id ,
routeName : context.session.matchedRoute?.name ,
targetHost : host ,
targetPort : port ,
error : error.message
} ,
success : false
} ) ;
// Handle as bounce
for ( const recipient of email . getAllRecipients ( ) ) {
await this . bounceManager . processSmtpFailure ( recipient , error . message , {
sender : email.from ,
originalEmailId : email.headers [ 'Message-ID' ] as string
} ) ;
}
throw error ;
}
}
/ * *
* Handle process action
* /
private async handleProcessAction ( action : IEmailAction , email : Email , context : IEmailContext ) : Promise < void > {
logger . log ( 'info' , ` Processing email with action options ` ) ;
// Apply scanning if requested
if ( action . process ? . scan ) {
// Use existing content scanner
// Note: ContentScanner integration would go here
logger . log ( 'info' , 'Content scanning requested' ) ;
}
// Note: DKIM signing will be applied at delivery time to ensure signature validity
// Queue for delivery
const queue = action . process ? . queue || 'normal' ;
await this . deliveryQueue . enqueue ( email , 'process' , context . session . matchedRoute ! ) ;
logger . log ( 'info' , ` Email queued for delivery in ${ queue } queue ` ) ;
}
/ * *
* Handle deliver action
* /
private async handleDeliverAction ( _action : IEmailAction , email : Email , context : IEmailContext ) : Promise < void > {
logger . log ( 'info' , ` Delivering email locally ` ) ;
// Queue for local delivery
await this . deliveryQueue . enqueue ( email , 'mta' , context . session . matchedRoute ! ) ;
logger . log ( 'info' , 'Email queued for local delivery' ) ;
}
/ * *
* Handle reject action
* /
private async handleRejectAction ( action : IEmailAction , email : Email , context : IEmailContext ) : Promise < void > {
const code = action . reject ? . code || 550 ;
const message = action . reject ? . message || 'Message rejected' ;
logger . log ( 'info' , ` Rejecting email with code ${ code } : ${ message } ` ) ;
SecurityLogger . getInstance ( ) . logEvent ( {
level : SecurityLogLevel.WARN ,
type : SecurityEventType . EMAIL_PROCESSING ,
message : 'Email rejected by routing rule' ,
ipAddress : context.session.remoteAddress ,
details : {
sessionId : context.session.id ,
routeName : context.session.matchedRoute?.name ,
rejectCode : code ,
rejectMessage : message ,
from : email . from ,
to : email.to
} ,
success : false
} ) ;
// Throw error with SMTP code and message
const error = new Error ( message ) ;
( error as any ) . responseCode = code ;
throw error ;
}
/ * *
* Set up DKIM configuration for all domains
* /
private async setupDkimForDomains ( ) : Promise < void > {
const domainConfigs = this . domainRegistry . getAllConfigs ( ) ;
if ( domainConfigs . length === 0 ) {
logger . log ( 'warn' , 'No domains configured for DKIM' ) ;
return ;
}
for ( const domainConfig of domainConfigs ) {
const domain = domainConfig . domain ;
const selector = domainConfig . dkim ? . selector || 'default' ;
try {
// Check if DKIM keys already exist for this domain
let keyPair : { privateKey : string ; publicKey : string } ;
try {
// Try to read existing keys
keyPair = await this . dkimCreator . readDKIMKeys ( domain ) ;
logger . log ( 'info' , ` Using existing DKIM keys for domain: ${ domain } ` ) ;
} catch ( error ) {
// Generate new keys if they don't exist
keyPair = await this . dkimCreator . createDKIMKeys ( ) ;
// Store them for future use
await this . dkimCreator . createAndStoreDKIMKeys ( domain ) ;
logger . log ( 'info' , ` Generated new DKIM keys for domain: ${ domain } ` ) ;
}
// Store the private key for signing
this . dkimKeys . set ( domain , keyPair . privateKey ) ;
// DNS record creation is now handled by DnsManager
logger . log ( 'info' , ` DKIM keys loaded for domain: ${ domain } with selector: ${ selector } ` ) ;
} catch ( error ) {
logger . log ( 'error' , ` Failed to set up DKIM for domain ${ domain } : ${ error . message } ` ) ;
}
}
}
/ * *
* Apply per - domain rate limits from domain configurations
* /
private applyDomainRateLimits ( ) : void {
const domainConfigs = this . domainRegistry . getAllConfigs ( ) ;
for ( const domainConfig of domainConfigs ) {
if ( domainConfig . rateLimits ) {
const domain = domainConfig . domain ;
const rateLimitConfig : any = { } ;
// Convert domain-specific rate limits to the format expected by UnifiedRateLimiter
if ( domainConfig . rateLimits . outbound ) {
if ( domainConfig . rateLimits . outbound . messagesPerMinute ) {
rateLimitConfig . maxMessagesPerMinute = domainConfig . rateLimits . outbound . messagesPerMinute ;
}
// Note: messagesPerHour and messagesPerDay would need additional implementation in rate limiter
}
if ( domainConfig . rateLimits . inbound ) {
if ( domainConfig . rateLimits . inbound . messagesPerMinute ) {
rateLimitConfig . maxMessagesPerMinute = domainConfig . rateLimits . inbound . messagesPerMinute ;
}
if ( domainConfig . rateLimits . inbound . connectionsPerIp ) {
rateLimitConfig . maxConnectionsPerIP = domainConfig . rateLimits . inbound . connectionsPerIp ;
}
if ( domainConfig . rateLimits . inbound . recipientsPerMessage ) {
rateLimitConfig . maxRecipientsPerMessage = domainConfig . rateLimits . inbound . recipientsPerMessage ;
}
}
// Apply the rate limits if we have any
if ( Object . keys ( rateLimitConfig ) . length > 0 ) {
this . rateLimiter . applyDomainLimits ( domain , rateLimitConfig ) ;
logger . log ( 'info' , ` Applied rate limits for domain ${ domain } : ` , rateLimitConfig ) ;
}
}
}
}
/ * *
* Check and rotate DKIM keys if needed
* /
private async checkAndRotateDkimKeys ( ) : Promise < void > {
const domainConfigs = this . domainRegistry . getAllConfigs ( ) ;
for ( const domainConfig of domainConfigs ) {
const domain = domainConfig . domain ;
const selector = domainConfig . dkim ? . selector || 'default' ;
const rotateKeys = domainConfig . dkim ? . rotateKeys || false ;
const rotationInterval = domainConfig . dkim ? . rotationInterval || 90 ;
const keySize = domainConfig . dkim ? . keySize || 2048 ;
if ( ! rotateKeys ) {
logger . log ( 'debug' , ` DKIM key rotation disabled for ${ domain } ` ) ;
continue ;
}
try {
// Check if keys need rotation
const needsRotation = await this . dkimCreator . needsRotation ( domain , selector , rotationInterval ) ;
if ( needsRotation ) {
logger . log ( 'info' , ` DKIM keys need rotation for ${ domain } (selector: ${ selector } ) ` ) ;
// Rotate the keys
const newSelector = await this . dkimCreator . rotateDkimKeys ( domain , selector , keySize ) ;
// Update the domain config with new selector
domainConfig . dkim = {
. . . domainConfig . dkim ,
selector : newSelector
} ;
// Re-register DNS handler for new selector if internal-dns mode
if ( domainConfig . dnsMode === 'internal-dns' && this . dcRouter . dnsServer ) {
// Get new public key
const keyPair = await this . dkimCreator . readDKIMKeysForSelector ( domain , newSelector ) ;
const publicKeyBase64 = keyPair . publicKey
. replace ( /-----BEGIN PUBLIC KEY-----/g , '' )
. replace ( /-----END PUBLIC KEY-----/g , '' )
. replace ( /\s/g , '' ) ;
const ttl = domainConfig . dns ? . internal ? . ttl || 3600 ;
// Register new selector
this . dcRouter . dnsServer . registerHandler (
` ${ newSelector } ._domainkey. ${ domain } ` ,
[ 'TXT' ] ,
( ) = > ( {
name : ` ${ newSelector } ._domainkey. ${ domain } ` ,
type : 'TXT' ,
class : 'IN' ,
ttl : ttl ,
data : ` v=DKIM1; k=rsa; p= ${ publicKeyBase64 } `
} )
) ;
logger . log ( 'info' , ` DKIM DNS handler registered for new selector: ${ newSelector } ._domainkey. ${ domain } ` ) ;
// Store the updated public key in storage
await this . dcRouter . storageManager . set (
` /email/dkim/ ${ domain } /public.key ` ,
keyPair . publicKey
) ;
}
// Clean up old keys after grace period (async, don't wait)
this . dkimCreator . cleanupOldKeys ( domain , 30 ) . catch ( error = > {
logger . log ( 'warn' , ` Failed to cleanup old DKIM keys for ${ domain } : ${ error . message } ` ) ;
} ) ;
} else {
logger . log ( 'debug' , ` DKIM keys for ${ domain } are up to date ` ) ;
}
} catch ( error ) {
logger . log ( 'error' , ` Failed to check/rotate DKIM keys for ${ domain } : ${ error . message } ` ) ;
}
}
}
/ * *
* Generate SmartProxy routes for email ports
* /
public generateProxyRoutes ( portMapping? : Record < number , number > ) : any [ ] {
const routes : any [ ] = [ ] ;
const defaultPortMapping = {
25 : 10025 ,
587 : 10587 ,
465 : 10465
} ;
const actualPortMapping = portMapping || defaultPortMapping ;
// Generate routes for each configured port
for ( const externalPort of this . options . ports ) {
const internalPort = actualPortMapping [ externalPort ] || externalPort + 10000 ;
let routeName = 'email-route' ;
let tlsMode = 'passthrough' ;
// Configure based on port
switch ( externalPort ) {
case 25 :
routeName = 'smtp-route' ;
tlsMode = 'passthrough' ; // STARTTLS
break ;
case 587 :
routeName = 'submission-route' ;
tlsMode = 'passthrough' ; // STARTTLS
break ;
case 465 :
routeName = 'smtps-route' ;
tlsMode = 'terminate' ; // Implicit TLS
break ;
default :
routeName = ` email-port- ${ externalPort } -route ` ;
}
routes . push ( {
name : routeName ,
match : {
ports : [ externalPort ]
} ,
action : {
type : 'forward' ,
target : {
host : 'localhost' ,
port : internalPort
} ,
tls : {
mode : tlsMode
}
}
} ) ;
}
return routes ;
}
/ * *
* Update server configuration
* /
public updateOptions ( options : Partial < IUnifiedEmailServerOptions > ) : void {
// Stop the server if changing ports
const portsChanged = options . ports &&
( ! this . options . ports ||
JSON . stringify ( options . ports ) !== JSON . stringify ( this . options . ports ) ) ;
if ( portsChanged ) {
this . stop ( ) . then ( ( ) = > {
this . options = { . . . this . options , . . . options } ;
this . start ( ) ;
} ) ;
} else {
// Update options without restart
this . options = { . . . this . options , . . . options } ;
// Update domain registry if domains changed
if ( options . domains ) {
this . domainRegistry = new DomainRegistry ( options . domains , options . defaults || this . options . defaults ) ;
}
// Update email router if routes changed
if ( options . routes ) {
this . emailRouter . updateRoutes ( options . routes ) ;
}
}
}
/ * *
* Update email routes
* /
public updateEmailRoutes ( routes : IEmailRoute [ ] ) : void {
this . options . routes = routes ;
this . emailRouter . updateRoutes ( routes ) ;
}
/ * *
* Get server statistics
* /
public getStats ( ) : IServerStats {
return { . . . this . stats } ;
}
/ * *
* Get domain registry
* /
public getDomainRegistry ( ) : DomainRegistry {
return this . domainRegistry ;
}
/ * *
* Update email routes dynamically
* /
public updateRoutes ( routes : IEmailRoute [ ] ) : void {
this . emailRouter . setRoutes ( routes ) ;
logger . log ( 'info' , ` Updated email routes with ${ routes . length } routes ` ) ;
}
/ * *
* Send an email through the delivery system
* @param email The email to send
* @param mode The processing mode to use
* @param rule Optional rule to apply
* @param options Optional sending options
* @returns The ID of the queued email
* /
public async sendEmail (
email : Email ,
mode : EmailProcessingMode = 'mta' ,
route? : IEmailRoute ,
options ? : {
skipSuppressionCheck? : boolean ;
ipAddress? : string ;
isTransactional? : boolean ;
}
) : Promise < string > {
logger . log ( 'info' , ` Sending email: ${ email . subject } to ${ email . to . join ( ', ' ) } ` ) ;
try {
// Validate the email
if ( ! email . from ) {
throw new Error ( 'Email must have a sender address' ) ;
}
if ( ! email . to || email . to . length === 0 ) {
throw new Error ( 'Email must have at least one recipient' ) ;
}
// Check if any recipients are on the suppression list (unless explicitly skipped)
if ( ! options ? . skipSuppressionCheck ) {
const suppressedRecipients = email . to . filter ( recipient = > this . isEmailSuppressed ( recipient ) ) ;
if ( suppressedRecipients . length > 0 ) {
// Filter out suppressed recipients
const originalCount = email . to . length ;
const suppressed = suppressedRecipients . map ( recipient = > {
const info = this . getSuppressionInfo ( recipient ) ;
return {
email : recipient ,
reason : info?.reason || 'Unknown' ,
until : info?.expiresAt ? new Date ( info . expiresAt ) . toISOString ( ) : 'permanent'
} ;
} ) ;
logger . log ( 'warn' , ` Filtering out ${ suppressedRecipients . length } suppressed recipient(s) ` , { suppressed } ) ;
// If all recipients are suppressed, throw an error
if ( suppressedRecipients . length === originalCount ) {
throw new Error ( 'All recipients are on the suppression list' ) ;
}
// Filter the recipients list to only include non-suppressed addresses
email . to = email . to . filter ( recipient = > ! this . isEmailSuppressed ( recipient ) ) ;
}
}
// Check if the sender domain has DKIM keys and sign the email if needed
if ( mode === 'mta' && route ? . action . options ? . mtaOptions ? . dkimSign ) {
const domain = email . from . split ( '@' ) [ 1 ] ;
await this . handleDkimSigning ( email , domain , route . action . options . mtaOptions . dkimOptions ? . keySelector || 'mta' ) ;
}
// Generate a unique ID for this email
const id = plugins . uuid . v4 ( ) ;
// Queue the email for delivery
await this . deliveryQueue . enqueue ( email , mode , route ) ;
// Record 'sent' event for domain reputation monitoring
const senderDomain = email . from . split ( '@' ) [ 1 ] ;
if ( senderDomain ) {
this . recordReputationEvent ( senderDomain , {
type : 'sent' ,
count : email.to.length
} ) ;
}
logger . log ( 'info' , ` Email queued with ID: ${ id } ` ) ;
return id ;
} catch ( error ) {
logger . log ( 'error' , ` Failed to send email: ${ error . message } ` ) ;
throw error ;
}
}
/ * *
* Handle DKIM signing for an email
* @param email The email to sign
* @param domain The domain to sign with
* @param selector The DKIM selector
* /
private async handleDkimSigning ( email : Email , domain : string , selector : string ) : Promise < void > {
try {
// Ensure we have DKIM keys for this domain
await this . dkimCreator . handleDKIMKeysForDomain ( domain ) ;
2026-02-10 20:30:43 +00:00
2025-10-24 08:09:29 +00:00
// Get the private key
const { privateKey } = await this . dkimCreator . readDKIMKeys ( domain ) ;
2026-02-10 20:30:43 +00:00
2025-10-24 08:09:29 +00:00
// Convert Email to raw format for signing
const rawEmail = email . toRFC822String ( ) ;
2026-02-10 20:30:43 +00:00
// Sign the email via Rust bridge
const signResult = await this . rustBridge . signDkim ( {
rawMessage : rawEmail ,
domain ,
selector ,
privateKey ,
2025-10-24 08:09:29 +00:00
} ) ;
2026-02-10 20:30:43 +00:00
if ( signResult . header ) {
email . addHeader ( 'DKIM-Signature' , signResult . header ) ;
2025-10-24 08:09:29 +00:00
logger . log ( 'info' , ` Successfully added DKIM signature for ${ domain } ` ) ;
}
} catch ( error ) {
logger . log ( 'error' , ` Failed to sign email with DKIM: ${ error . message } ` ) ;
// Continue without DKIM rather than failing the send
}
}
/ * *
* Process a bounce notification email
* @param bounceEmail The email containing bounce notification information
* @returns Processed bounce record or null if not a bounce
* /
public async processBounceNotification ( bounceEmail : Email ) : Promise < boolean > {
logger . log ( 'info' , 'Processing potential bounce notification email' ) ;
try {
// Process as a bounce notification (no conversion needed anymore)
const bounceRecord = await this . bounceManager . processBounceEmail ( bounceEmail ) ;
if ( bounceRecord ) {
logger . log ( 'info' , ` Successfully processed bounce notification for ${ bounceRecord . recipient } ` , {
bounceType : bounceRecord.bounceType ,
bounceCategory : bounceRecord.bounceCategory
} ) ;
// Notify any registered listeners about the bounce
this . emit ( 'bounceProcessed' , bounceRecord ) ;
// Record bounce event for domain reputation tracking
if ( bounceRecord . domain ) {
this . recordReputationEvent ( bounceRecord . domain , {
type : 'bounce' ,
hardBounce : bounceRecord.bounceCategory === BounceCategory . HARD ,
receivingDomain : bounceRecord.recipient.split ( '@' ) [ 1 ]
} ) ;
}
// Log security event
SecurityLogger . getInstance ( ) . logEvent ( {
level : SecurityLogLevel.INFO ,
type : SecurityEventType . EMAIL_VALIDATION ,
message : ` Bounce notification processed for recipient ` ,
domain : bounceRecord.domain ,
details : {
recipient : bounceRecord.recipient ,
bounceType : bounceRecord.bounceType ,
bounceCategory : bounceRecord.bounceCategory
} ,
success : true
} ) ;
return true ;
} else {
logger . log ( 'info' , 'Email not recognized as a bounce notification' ) ;
return false ;
}
} catch ( error ) {
logger . log ( 'error' , ` Error processing bounce notification: ${ error . message } ` ) ;
SecurityLogger . getInstance ( ) . logEvent ( {
level : SecurityLogLevel.ERROR ,
type : SecurityEventType . EMAIL_VALIDATION ,
message : 'Failed to process bounce notification' ,
details : {
error : error.message ,
subject : bounceEmail.subject
} ,
success : false
} ) ;
return false ;
}
}
/ * *
* Process an SMTP failure as a bounce
* @param recipient Recipient email that failed
* @param smtpResponse SMTP error response
* @param options Additional options for bounce processing
* @returns Processed bounce record
* /
public async processSmtpFailure (
recipient : string ,
smtpResponse : string ,
options : {
sender? : string ;
originalEmailId? : string ;
statusCode? : string ;
headers? : Record < string , string > ;
} = { }
) : Promise < boolean > {
logger . log ( 'info' , ` Processing SMTP failure for ${ recipient } : ${ smtpResponse } ` ) ;
try {
// Process the SMTP failure through the bounce manager
const bounceRecord = await this . bounceManager . processSmtpFailure (
recipient ,
smtpResponse ,
options
) ;
logger . log ( 'info' , ` Successfully processed SMTP failure for ${ recipient } as ${ bounceRecord . bounceCategory } bounce ` , {
bounceType : bounceRecord.bounceType
} ) ;
// Notify any registered listeners about the bounce
this . emit ( 'bounceProcessed' , bounceRecord ) ;
// Record bounce event for domain reputation tracking
if ( bounceRecord . domain ) {
this . recordReputationEvent ( bounceRecord . domain , {
type : 'bounce' ,
hardBounce : bounceRecord.bounceCategory === BounceCategory . HARD ,
receivingDomain : bounceRecord.recipient.split ( '@' ) [ 1 ]
} ) ;
}
// Log security event
SecurityLogger . getInstance ( ) . logEvent ( {
level : SecurityLogLevel.INFO ,
type : SecurityEventType . EMAIL_VALIDATION ,
message : ` SMTP failure processed for recipient ` ,
domain : bounceRecord.domain ,
details : {
recipient : bounceRecord.recipient ,
bounceType : bounceRecord.bounceType ,
bounceCategory : bounceRecord.bounceCategory ,
smtpResponse
} ,
success : true
} ) ;
return true ;
} catch ( error ) {
logger . log ( 'error' , ` Error processing SMTP failure: ${ error . message } ` ) ;
SecurityLogger . getInstance ( ) . logEvent ( {
level : SecurityLogLevel.ERROR ,
type : SecurityEventType . EMAIL_VALIDATION ,
message : 'Failed to process SMTP failure' ,
details : {
recipient ,
smtpResponse ,
error : error.message
} ,
success : false
} ) ;
return false ;
}
}
/ * *
* Check if an email address is suppressed ( has bounced previously )
* @param email Email address to check
* @returns Whether the email is suppressed
* /
public isEmailSuppressed ( email : string ) : boolean {
return this . bounceManager . isEmailSuppressed ( email ) ;
}
/ * *
* Get suppression information for an email
* @param email Email address to check
* @returns Suppression information or null if not suppressed
* /
public getSuppressionInfo ( email : string ) : {
reason : string ;
timestamp : number ;
expiresAt? : number ;
} | null {
return this . bounceManager . getSuppressionInfo ( email ) ;
}
/ * *
* Get bounce history information for an email
* @param email Email address to check
* @returns Bounce history or null if no bounces
* /
public getBounceHistory ( email : string ) : {
lastBounce : number ;
count : number ;
type : BounceType ;
category : BounceCategory ;
} | null {
return this . bounceManager . getBounceInfo ( email ) ;
}
/ * *
* Get all suppressed email addresses
* @returns Array of suppressed email addresses
* /
public getSuppressionList ( ) : string [ ] {
return this . bounceManager . getSuppressionList ( ) ;
}
/ * *
* Get all hard bounced email addresses
* @returns Array of hard bounced email addresses
* /
public getHardBouncedAddresses ( ) : string [ ] {
return this . bounceManager . getHardBouncedAddresses ( ) ;
}
/ * *
* Add an email to the suppression list
* @param email Email address to suppress
* @param reason Reason for suppression
* @param expiresAt Optional expiration time ( undefined for permanent )
* /
public addToSuppressionList ( email : string , reason : string , expiresAt? : number ) : void {
this . bounceManager . addToSuppressionList ( email , reason , expiresAt ) ;
logger . log ( 'info' , ` Added ${ email } to suppression list: ${ reason } ` ) ;
}
/ * *
* Remove an email from the suppression list
* @param email Email address to remove from suppression
* /
public removeFromSuppressionList ( email : string ) : void {
this . bounceManager . removeFromSuppressionList ( email ) ;
logger . log ( 'info' , ` Removed ${ email } from suppression list ` ) ;
}
/ * *
2026-02-10 23:23:00 +00:00
* Record an email event for domain reputation tracking .
* Currently a no - op — the sender reputation monitor is not yet implemented .
2025-10-24 08:09:29 +00:00
* @param domain Domain sending the email
* @param event Event details
* /
public recordReputationEvent ( domain : string , event : {
type : 'sent' | 'delivered' | 'bounce' | 'complaint' | 'open' | 'click' ;
count? : number ;
hardBounce? : boolean ;
receivingDomain? : string ;
} ) : void {
2026-02-10 23:23:00 +00:00
logger . log ( 'debug' , ` Reputation event for ${ domain } : ${ event . type } ` ) ;
2025-10-24 08:09:29 +00:00
}
/ * *
* Check if DKIM key exists for a domain
* @param domain Domain to check
* /
public hasDkimKey ( domain : string ) : boolean {
return this . dkimKeys . has ( domain ) ;
}
/ * *
* Record successful email delivery
* @param domain Sending domain
* /
public recordDelivery ( domain : string ) : void {
this . recordReputationEvent ( domain , {
type : 'delivered' ,
count : 1
} ) ;
}
/ * *
* Record email bounce
* @param domain Sending domain
* @param receivingDomain Receiving domain that bounced
* @param bounceType Type of bounce ( hard / soft )
* @param reason Bounce reason
* /
public recordBounce ( domain : string , receivingDomain : string , bounceType : 'hard' | 'soft' , reason : string ) : void {
// Record bounce in bounce manager
const bounceRecord = {
id : ` bounce_ ${ Date . now ( ) } _ ${ Math . random ( ) . toString ( 36 ) . substring ( 2 , 9 ) } ` ,
recipient : ` user@ ${ receivingDomain } ` ,
sender : ` user@ ${ domain } ` ,
domain : domain ,
bounceType : bounceType === 'hard' ? BounceType.INVALID_RECIPIENT : BounceType.TEMPORARY_FAILURE ,
bounceCategory : bounceType === 'hard' ? BounceCategory.HARD : BounceCategory.SOFT ,
timestamp : Date.now ( ) ,
smtpResponse : reason ,
diagnosticCode : reason ,
statusCode : bounceType === 'hard' ? '550' : '450' ,
processed : false
} ;
// Process the bounce
this . bounceManager . processBounce ( bounceRecord ) ;
// Record reputation event
this . recordReputationEvent ( domain , {
type : 'bounce' ,
count : 1 ,
hardBounce : bounceType === 'hard' ,
receivingDomain
} ) ;
}
/ * *
* Get the rate limiter instance
* @returns The unified rate limiter
* /
public getRateLimiter ( ) : UnifiedRateLimiter {
return this . rateLimiter ;
}
}