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 15:31:31 +00:00
// Deliverability types (IPWarmupManager and SenderReputationMonitor are optional external modules)
interface IIPWarmupConfig {
enabled? : boolean ;
ips? : string [ ] ;
[ key : string ] : any ;
}
interface IReputationMonitorConfig {
enabled? : boolean ;
domains? : string [ ] ;
[ key : string ] : any ;
}
interface IPWarmupManager {
getWarmupStatus ( ip : string ) : any ;
addIPToWarmup ( ip : string , config? : any ) : void ;
removeIPFromWarmup ( ip : string ) : void ;
updateMetrics ( ip : string , metrics : any ) : void ;
canSendMoreToday ( ip : string ) : boolean ;
canSendMoreThisHour ( ip : string ) : boolean ;
getBestIPForSending ( . . . args : any [ ] ) : string | null ;
setActiveAllocationPolicy ( policy : string ) : void ;
recordSend ( . . . args : any [ ] ) : void ;
}
interface SenderReputationMonitor {
getReputationData ( domain : string ) : any ;
getReputationSummary ( ) : any ;
addDomain ( domain : string ) : void ;
removeDomain ( domain : string ) : void ;
recordSendEvent ( domain : string , event : any ) : void ;
}
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 { createSmtpServer } from '../delivery/smtpserver/index.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 ;
// Deliverability options
ipWarmupConfig? : IIPWarmupConfig ;
reputationMonitorConfig? : IReputationMonitorConfig ;
}
/ * *
* 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 ;
2026-02-10 15:31:31 +00:00
private ipWarmupManager : IPWarmupManager | null ;
private senderReputationMonitor : SenderReputationMonitor | null ;
2025-10-24 08:09:29 +00:00
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
} ) ;
2026-02-10 15:31:31 +00:00
// IP warmup manager and sender reputation monitor are optional
// They will be initialized when the deliverability module is available
this . ipWarmupManager = null ;
this . senderReputationMonitor = null ;
2025-10-24 08:09:29 +00:00
// 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
// Start Rust security bridge (non-blocking — server works without it)
const bridgeOk = await this . rustBridge . start ( ) ;
if ( bridgeOk ) {
logger . log ( 'info' , 'Rust security bridge started — using Rust for DKIM/SPF/DMARC verification' ) ;
} else {
logger . log ( 'warn' , 'Rust security bridge unavailable — falling back to TypeScript security verification' ) ;
}
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' ) ;
// Skip server creation in socket-handler mode
if ( this . options . useSocketHandler ) {
logger . log ( 'info' , 'UnifiedEmailServer started in socket-handler mode (no port listening)' ) ;
this . emit ( 'started' ) ;
return ;
}
// Ensure we have the necessary TLS options
const hasTlsConfig = this . options . tls ? . keyPath && this . options . tls ? . certPath ;
// Prepare the certificate and key if available
let key : string | undefined ;
let cert : string | undefined ;
if ( hasTlsConfig ) {
try {
key = plugins . fs . readFileSync ( this . options . tls . keyPath ! , 'utf8' ) ;
cert = plugins . fs . readFileSync ( this . options . tls . certPath ! , 'utf8' ) ;
logger . log ( 'info' , 'TLS certificates loaded successfully' ) ;
} catch ( error ) {
logger . log ( 'warn' , ` Failed to load TLS certificates: ${ error . message } ` ) ;
}
}
// Create a SMTP server for each port
for ( const port of this . options . ports as number [ ] ) {
// Create a reference object to hold the MTA service during setup
const mtaRef = {
config : {
smtp : {
hostname : this.options.hostname
} ,
security : {
checkIPReputation : false ,
verifyDkim : true ,
verifySpf : true ,
verifyDmarc : true
}
} ,
2026-02-10 16:25:55 +00:00
// Security verification delegated to the Rust bridge when available
2025-10-24 08:09:29 +00:00
dkimVerifier : {
2026-02-10 16:25:55 +00:00
verify : async ( rawMessage : string ) = > {
if ( this . rustBridge . running ) {
try {
const results = await this . rustBridge . verifyDkim ( rawMessage ) ;
const first = results [ 0 ] ;
return { isValid : first?.is_valid ? ? false , domain : first?.domain ? ? '' } ;
} catch ( err ) {
logger . log ( 'warn' , ` Rust DKIM verification failed, accepting: ${ ( err as Error ) . message } ` ) ;
return { isValid : true , domain : '' } ;
}
}
return { isValid : true , domain : '' } ; // No bridge — accept
}
2025-10-24 08:09:29 +00:00
} ,
spfVerifier : {
2026-02-10 16:25:55 +00:00
verifyAndApply : async ( session : any ) = > {
if ( this . rustBridge . running && session ? . remoteAddress && session . remoteAddress !== '127.0.0.1' ) {
try {
const result = await this . rustBridge . checkSpf ( {
ip : session.remoteAddress ,
heloDomain : session.clientHostname || '' ,
hostname : this.options.hostname ,
mailFrom : session.envelope?.mailFrom?.address || session . mailFrom || '' ,
} ) ;
return result . result === 'pass' || result . result === 'none' || result . result === 'neutral' ;
} catch ( err ) {
logger . log ( 'warn' , ` Rust SPF check failed, accepting: ${ ( err as Error ) . message } ` ) ;
return true ;
}
}
return true ; // No bridge or localhost — accept
}
2025-10-24 08:09:29 +00:00
} ,
dmarcVerifier : {
verify : async ( ) = > ( { } ) ,
applyPolicy : ( ) = > true
} ,
processIncomingEmail : async ( email : Email ) = > {
// Process email using the new route-based system
await this . processEmailByMode ( email , {
id : 'session-' + Math . random ( ) . toString ( 36 ) . substring ( 2 ) ,
state : SmtpState.FINISHED ,
mailFrom : email.from ,
rcptTo : email.to ,
emailData : email.toRFC822String ( ) , // Use the proper method to get the full email content
useTLS : false ,
connectionEnded : true ,
remoteAddress : '127.0.0.1' ,
clientHostname : '' ,
secure : false ,
authenticated : false ,
envelope : {
mailFrom : { address : email.from , args : { } } ,
rcptTo : email.to.map ( recipient = > ( { address : recipient , args : { } } ) )
}
} ) ;
return true ;
}
} ;
// Create server options
const serverOptions = {
port ,
hostname : this.options.hostname ,
key ,
cert
} ;
// Create and start the SMTP server
const smtpServer = createSmtpServer ( mtaRef as any , serverOptions ) ;
this . servers . push ( smtpServer ) ;
// Start the server
await new Promise < void > ( ( resolve , reject ) = > {
try {
// Leave this empty for now, smtpServer.start() is handled by the SMTPServer class internally
// The server is started when it's created
logger . log ( 'info' , ` UnifiedEmailServer listening on port ${ port } ` ) ;
// Event handlers are managed internally by the SmtpServer class
// No need to access the private server property
resolve ( ) ;
} catch ( err ) {
if ( ( err as any ) . code === 'EADDRINUSE' ) {
logger . log ( 'error' , ` Port ${ port } is already in use ` ) ;
reject ( new Error ( ` Port ${ port } is already in use ` ) ) ;
} else {
logger . log ( 'error' , ` Error starting server on port ${ port } : ${ err . message } ` ) ;
reject ( err ) ;
}
}
} ) ;
}
logger . log ( 'info' , 'UnifiedEmailServer started successfully' ) ;
this . emit ( 'started' ) ;
} catch ( error ) {
logger . log ( 'error' , ` Failed to start UnifiedEmailServer: ${ error . message } ` ) ;
throw error ;
}
}
/ * *
* Handle a socket from smartproxy in socket - handler mode
* @param socket The socket to handle
* @param port The port this connection is for ( 25 , 587 , 465 )
* /
public async handleSocket ( socket : plugins.net.Socket | plugins . tls . TLSSocket , port : number ) : Promise < void > {
if ( ! this . options . useSocketHandler ) {
logger . log ( 'error' , 'handleSocket called but useSocketHandler is not enabled' ) ;
socket . destroy ( ) ;
return ;
}
logger . log ( 'info' , ` Handling socket for port ${ port } ` ) ;
// Create a temporary SMTP server instance for this connection
// We need a full server instance because the SMTP protocol handler needs all components
const smtpServerOptions = {
port ,
hostname : this.options.hostname ,
key : this.options.tls?.keyPath ? plugins . fs . readFileSync ( this . options . tls . keyPath , 'utf8' ) : undefined ,
cert : this.options.tls?.certPath ? plugins . fs . readFileSync ( this . options . tls . certPath , 'utf8' ) : undefined
} ;
// Create the SMTP server instance
const smtpServer = createSmtpServer ( this , smtpServerOptions ) ;
// Get the connection manager from the server
const connectionManager = ( smtpServer as any ) . connectionManager ;
if ( ! connectionManager ) {
logger . log ( 'error' , 'Could not get connection manager from SMTP server' ) ;
socket . destroy ( ) ;
return ;
}
// Determine if this is a secure connection
// Port 465 uses implicit TLS, so the socket is already secure
const isSecure = port === 465 || socket instanceof plugins . tls . TLSSocket ;
// Pass the socket to the connection manager
try {
await connectionManager . handleConnection ( socket , isSecure ) ;
} catch ( error ) {
logger . log ( 'error' , ` Error handling socket connection: ${ error . message } ` ) ;
socket . destroy ( ) ;
}
}
/ * *
* Stop the unified email server
* /
public async stop ( ) : Promise < void > {
logger . log ( 'info' , 'Stopping UnifiedEmailServer' ) ;
try {
// Clear the servers array - servers will be garbage collected
this . servers = [ ] ;
2026-02-10 16:25:55 +00:00
// Stop Rust security bridge
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 16:25:55 +00:00
/ * *
* Verify inbound email security ( DKIM / SPF / DMARC ) using the Rust bridge .
* Falls back gracefully if the bridge is not running .
* /
private async verifyInboundSecurity ( email : Email , session : IExtendedSmtpSession ) : Promise < void > {
if ( ! this . rustBridge . running ) {
return ; // Bridge not available — skip verification
}
try {
const rawMessage = session . emailData || email . toRFC822String ( ) ;
const result = await this . rustBridge . verifyEmail ( {
rawMessage ,
ip : session.remoteAddress ,
heloDomain : session.clientHostname || '' ,
hostname : this.options.hostname ,
mailFrom : session.envelope?.mailFrom?.address || session . mailFrom || '' ,
} ) ;
// Apply DKIM result headers
if ( result . dkim && result . dkim . length > 0 ) {
const dkimSummary = result . dkim
. map ( d = > ` ${ d . status } ${ d . domain ? ` ( ${ d . domain } ) ` : '' } ` )
. join ( ', ' ) ;
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 ` ) ;
}
}
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 ;
}
/ * *
* Handle email in MTA mode ( programmatic processing )
* /
private async _handleMtaMode ( email : Email , session : IExtendedSmtpSession ) : Promise < void > {
logger . log ( 'info' , ` Handling email in MTA mode for session ${ session . id } ` ) ;
try {
// Apply MTA rule options if provided
if ( session . matchedRoute ? . action . options ? . mtaOptions ) {
const options = session . matchedRoute . action . options . mtaOptions ;
// Apply DKIM signing if enabled
if ( options . dkimSign && options . dkimOptions ) {
// Sign the email with DKIM
logger . log ( 'info' , ` Signing email with DKIM for domain ${ options . dkimOptions . domainName } ` ) ;
try {
// Ensure DKIM keys exist for the domain
await this . dkimCreator . handleDKIMKeysForDomain ( options . dkimOptions . domainName ) ;
// Convert Email to raw format for signing
const rawEmail = email . toRFC822String ( ) ;
// Create headers object
const headers = { } ;
for ( const [ key , value ] of Object . entries ( email . headers ) ) {
headers [ key ] = value ;
}
// Sign the email
2026-02-10 15:31:31 +00:00
const dkimDomain = options . dkimOptions . domainName ;
const dkimSelector = options . dkimOptions . keySelector || 'mta' ;
const dkimPrivateKey = ( await this . dkimCreator . readDKIMKeys ( dkimDomain ) ) . privateKey ;
2025-10-24 08:09:29 +00:00
const signResult = await plugins . dkimSign ( rawEmail , {
2026-02-10 15:31:31 +00:00
signingDomain : dkimDomain ,
selector : dkimSelector ,
privateKey : dkimPrivateKey ,
2025-10-24 08:09:29 +00:00
canonicalization : 'relaxed/relaxed' ,
algorithm : 'rsa-sha256' ,
signTime : new Date ( ) ,
signatureData : [
{
2026-02-10 15:31:31 +00:00
signingDomain : dkimDomain ,
selector : dkimSelector ,
privateKey : dkimPrivateKey ,
2025-10-24 08:09:29 +00:00
algorithm : 'rsa-sha256' ,
canonicalization : 'relaxed/relaxed'
}
]
} ) ;
// Add the DKIM-Signature header to the email
if ( signResult . signatures ) {
email . addHeader ( 'DKIM-Signature' , signResult . signatures ) ;
logger . log ( 'info' , ` Successfully added DKIM signature for ${ options . dkimOptions . domainName } ` ) ;
}
} catch ( error ) {
logger . log ( 'error' , ` Failed to sign email with DKIM: ${ error . message } ` ) ;
}
}
}
// Get email content for logging/processing
const subject = email . subject ;
const recipients = email . getAllRecipients ( ) . join ( ', ' ) ;
logger . log ( 'info' , ` Email processed by MTA: ${ subject } to ${ recipients } ` ) ;
SecurityLogger . getInstance ( ) . logEvent ( {
level : SecurityLogLevel.INFO ,
type : SecurityEventType . EMAIL_PROCESSING ,
message : 'Email processed by MTA' ,
ipAddress : session.remoteAddress ,
details : {
sessionId : session.id ,
ruleName : session.matchedRoute?.name || 'default' ,
subject ,
recipients
} ,
success : true
} ) ;
} catch ( error ) {
logger . log ( 'error' , ` Failed to process email in MTA mode: ${ error . message } ` ) ;
SecurityLogger . getInstance ( ) . logEvent ( {
level : SecurityLogLevel.ERROR ,
type : SecurityEventType . EMAIL_PROCESSING ,
message : 'MTA processing failed' ,
ipAddress : session.remoteAddress ,
details : {
sessionId : session.id ,
ruleName : session.matchedRoute?.name || 'default' ,
error : error.message
} ,
success : false
} ) ;
throw error ;
}
}
/ * *
* Handle email in process mode ( store - and - forward with scanning )
* /
private async _handleProcessMode ( email : Email , session : IExtendedSmtpSession ) : Promise < void > {
logger . log ( 'info' , ` Handling email in process mode for session ${ session . id } ` ) ;
try {
const route = session . matchedRoute ;
// Apply content scanning if enabled
if ( route ? . action . options ? . contentScanning && route . action . options . scanners && route . action . options . scanners . length > 0 ) {
logger . log ( 'info' , 'Performing content scanning' ) ;
// Apply each scanner
for ( const scanner of route . action . options . scanners ) {
switch ( scanner . type ) {
case 'spam' :
logger . log ( 'info' , 'Scanning for spam content' ) ;
// Implement spam scanning
break ;
case 'virus' :
logger . log ( 'info' , 'Scanning for virus content' ) ;
// Implement virus scanning
break ;
case 'attachment' :
logger . log ( 'info' , 'Scanning attachments' ) ;
// Check for blocked extensions
if ( scanner . blockedExtensions && scanner . blockedExtensions . length > 0 ) {
for ( const attachment of email . attachments ) {
const ext = this . getFileExtension ( attachment . filename ) ;
if ( scanner . blockedExtensions . includes ( ext ) ) {
if ( scanner . action === 'reject' ) {
throw new Error ( ` Blocked attachment type: ${ ext } ` ) ;
} else { // tag
email . addHeader ( 'X-Attachment-Warning' , ` Potentially unsafe attachment: ${ attachment . filename } ` ) ;
}
}
}
}
break ;
}
}
}
// Apply transformations if defined
if ( route ? . action . options ? . transformations && route . action . options . transformations . length > 0 ) {
logger . log ( 'info' , 'Applying email transformations' ) ;
for ( const transform of route . action . options . transformations ) {
switch ( transform . type ) {
case 'addHeader' :
if ( transform . header && transform . value ) {
email . addHeader ( transform . header , transform . value ) ;
}
break ;
}
}
}
logger . log ( 'info' , ` Email successfully processed in store-and-forward mode ` ) ;
SecurityLogger . getInstance ( ) . logEvent ( {
level : SecurityLogLevel.INFO ,
type : SecurityEventType . EMAIL_PROCESSING ,
message : 'Email processed and queued' ,
ipAddress : session.remoteAddress ,
details : {
sessionId : session.id ,
ruleName : route?.name || 'default' ,
contentScanning : route?.action.options?.contentScanning || false ,
subject : email.subject
} ,
success : true
} ) ;
} catch ( error ) {
logger . log ( 'error' , ` Failed to process email: ${ error . message } ` ) ;
SecurityLogger . getInstance ( ) . logEvent ( {
level : SecurityLogLevel.ERROR ,
type : SecurityEventType . EMAIL_PROCESSING ,
message : 'Email processing failed' ,
ipAddress : session.remoteAddress ,
details : {
sessionId : session.id ,
ruleName : session.matchedRoute?.name || 'default' ,
error : error.message
} ,
success : false
} ) ;
throw error ;
}
}
/ * *
* Get file extension from filename
* /
private getFileExtension ( filename : string ) : string {
return filename . substring ( filename . lastIndexOf ( '.' ) ) . toLowerCase ( ) ;
}
/ * *
* 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 ) ) ;
}
}
// IP warmup handling
let ipAddress = options ? . ipAddress ;
// If no specific IP was provided, use IP warmup manager to find the best IP
if ( ! ipAddress ) {
const domain = email . from . split ( '@' ) [ 1 ] ;
ipAddress = this . getBestIPForSending ( {
from : email . from ,
to : email.to ,
domain ,
isTransactional : options?.isTransactional
} ) ;
if ( ipAddress ) {
logger . log ( 'info' , ` Selected IP ${ ipAddress } for sending based on warmup status ` ) ;
}
}
// If an IP is provided or selected by warmup manager, check its capacity
if ( ipAddress ) {
// Check if the IP can send more today
if ( ! this . canIPSendMoreToday ( ipAddress ) ) {
logger . log ( 'warn' , ` IP ${ ipAddress } has reached its daily sending limit, email will be queued for later delivery ` ) ;
}
// Check if the IP can send more this hour
if ( ! this . canIPSendMoreThisHour ( ipAddress ) ) {
logger . log ( 'warn' , ` IP ${ ipAddress } has reached its hourly sending limit, email will be queued for later delivery ` ) ;
}
// Record the send for IP warmup tracking
this . recordIPSend ( ipAddress ) ;
// Add IP header to the email
email . addHeader ( 'X-Sending-IP' , ipAddress ) ;
}
// 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 ) ;
// Get the private key
const { privateKey } = await this . dkimCreator . readDKIMKeys ( domain ) ;
// Convert Email to raw format for signing
const rawEmail = email . toRFC822String ( ) ;
// Sign the email
const signResult = await plugins . dkimSign ( rawEmail , {
2026-02-10 15:31:31 +00:00
signingDomain : domain ,
selector : selector ,
privateKey : privateKey ,
2025-10-24 08:09:29 +00:00
canonicalization : 'relaxed/relaxed' ,
algorithm : 'rsa-sha256' ,
signTime : new Date ( ) ,
signatureData : [
{
signingDomain : domain ,
selector : selector ,
privateKey : privateKey ,
algorithm : 'rsa-sha256' ,
canonicalization : 'relaxed/relaxed'
}
]
} ) ;
// Add the DKIM-Signature header to the email
if ( signResult . signatures ) {
email . addHeader ( 'DKIM-Signature' , signResult . signatures ) ;
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 ` ) ;
}
/ * *
* Get the status of IP warmup process
* @param ipAddress Optional specific IP to check
* @returns Status of IP warmup
* /
public getIPWarmupStatus ( ipAddress? : string ) : any {
return this . ipWarmupManager . getWarmupStatus ( ipAddress ) ;
}
/ * *
* Add a new IP address to the warmup process
* @param ipAddress IP address to add
* /
public addIPToWarmup ( ipAddress : string ) : void {
this . ipWarmupManager . addIPToWarmup ( ipAddress ) ;
}
/ * *
* Remove an IP address from the warmup process
* @param ipAddress IP address to remove
* /
public removeIPFromWarmup ( ipAddress : string ) : void {
this . ipWarmupManager . removeIPFromWarmup ( ipAddress ) ;
}
/ * *
* Update metrics for an IP in the warmup process
* @param ipAddress IP address
* @param metrics Metrics to update
* /
public updateIPWarmupMetrics (
ipAddress : string ,
metrics : { openRate? : number ; bounceRate? : number ; complaintRate? : number }
) : void {
this . ipWarmupManager . updateMetrics ( ipAddress , metrics ) ;
}
/ * *
* Check if an IP can send more emails today
* @param ipAddress IP address to check
* @returns Whether the IP can send more today
* /
public canIPSendMoreToday ( ipAddress : string ) : boolean {
return this . ipWarmupManager . canSendMoreToday ( ipAddress ) ;
}
/ * *
* Check if an IP can send more emails in the current hour
* @param ipAddress IP address to check
* @returns Whether the IP can send more this hour
* /
public canIPSendMoreThisHour ( ipAddress : string ) : boolean {
return this . ipWarmupManager . canSendMoreThisHour ( ipAddress ) ;
}
/ * *
* Get the best IP to use for sending an email based on warmup status
* @param emailInfo Information about the email being sent
* @returns Best IP to use or null
* /
public getBestIPForSending ( emailInfo : {
from : string ;
to : string [ ] ;
domain : string ;
isTransactional? : boolean ;
} ) : string | null {
return this . ipWarmupManager . getBestIPForSending ( emailInfo ) ;
}
/ * *
* Set the active IP allocation policy for warmup
* @param policyName Name of the policy to set
* /
public setIPAllocationPolicy ( policyName : string ) : void {
this . ipWarmupManager . setActiveAllocationPolicy ( policyName ) ;
}
/ * *
* Record that an email was sent using a specific IP
* @param ipAddress IP address used for sending
* /
public recordIPSend ( ipAddress : string ) : void {
this . ipWarmupManager . recordSend ( ipAddress ) ;
}
/ * *
* Get reputation data for a domain
* @param domain Domain to get reputation for
* @returns Domain reputation metrics
* /
public getDomainReputationData ( domain : string ) : any {
return this . senderReputationMonitor . getReputationData ( domain ) ;
}
/ * *
* Get summary reputation data for all monitored domains
* @returns Summary data for all domains
* /
public getReputationSummary ( ) : any {
return this . senderReputationMonitor . getReputationSummary ( ) ;
}
/ * *
* Add a domain to the reputation monitoring system
* @param domain Domain to add
* /
public addDomainToMonitoring ( domain : string ) : void {
this . senderReputationMonitor . addDomain ( domain ) ;
}
/ * *
* Remove a domain from the reputation monitoring system
* @param domain Domain to remove
* /
public removeDomainFromMonitoring ( domain : string ) : void {
this . senderReputationMonitor . removeDomain ( domain ) ;
}
/ * *
* Record an email event for domain reputation tracking
* @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 {
this . senderReputationMonitor . recordSendEvent ( domain , event ) ;
}
/ * *
* 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 ;
}
}