update
This commit is contained in:
@ -1,6 +1,6 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import * as paths from '../../paths.js';
|
||||
import type { MtaService } from '../delivery/classes.mta.js';
|
||||
import { DKIMCreator } from '../security/classes.dkimcreator.js';
|
||||
|
||||
/**
|
||||
* Interface for DNS record information
|
||||
@ -39,15 +39,15 @@ export interface IDnsVerificationResult {
|
||||
* Manager for DNS-related operations, including record lookups, verification, and generation
|
||||
*/
|
||||
export class DNSManager {
|
||||
public mtaRef: MtaService;
|
||||
public dkimCreator: DKIMCreator;
|
||||
private cache: Map<string, { data: any; expires: number }> = new Map();
|
||||
private defaultOptions: IDnsLookupOptions = {
|
||||
cacheTtl: 300000, // 5 minutes
|
||||
timeout: 5000 // 5 seconds
|
||||
};
|
||||
|
||||
constructor(mtaRefArg: MtaService, options?: IDnsLookupOptions) {
|
||||
this.mtaRef = mtaRefArg;
|
||||
constructor(dkimCreatorArg: DKIMCreator, options?: IDnsLookupOptions) {
|
||||
this.dkimCreator = dkimCreatorArg;
|
||||
|
||||
if (options) {
|
||||
this.defaultOptions = {
|
||||
@ -529,8 +529,8 @@ export class DNSManager {
|
||||
|
||||
// Get DKIM record (already created by DKIMCreator)
|
||||
try {
|
||||
// Now using the public method
|
||||
const dkimRecord = await this.mtaRef.dkimCreator.getDNSRecordForDomain(domain);
|
||||
// Call the DKIM creator directly
|
||||
const dkimRecord = await this.dkimCreator.getDNSRecordForDomain(domain);
|
||||
records.push(dkimRecord);
|
||||
} catch (error) {
|
||||
console.error(`Error getting DKIM record for ${domain}:`, error);
|
||||
|
@ -98,7 +98,7 @@ export interface IMtaOptions {
|
||||
dkimOptions?: {
|
||||
domainName: string;
|
||||
keySelector: string;
|
||||
privateKey: string;
|
||||
privateKey?: string;
|
||||
};
|
||||
smtpBanner?: string;
|
||||
maxConnections?: number;
|
||||
|
@ -7,6 +7,14 @@ import {
|
||||
SecurityLogLevel,
|
||||
SecurityEventType
|
||||
} from '../../security/index.js';
|
||||
import { DKIMCreator } from '../security/classes.dkimcreator.js';
|
||||
import { IPReputationChecker } from '../../security/classes.ipreputationchecker.js';
|
||||
import {
|
||||
IPWarmupManager,
|
||||
type IIPWarmupConfig,
|
||||
SenderReputationMonitor,
|
||||
type IReputationMonitorConfig
|
||||
} from '../../deliverability/index.js';
|
||||
import { DomainRouter } from './classes.domain.router.js';
|
||||
import type {
|
||||
IEmailConfig,
|
||||
@ -14,10 +22,13 @@ import type {
|
||||
IDomainRule
|
||||
} from './classes.email.config.js';
|
||||
import { Email } from '../core/classes.email.js';
|
||||
import { BounceManager, BounceType, BounceCategory } from '../core/classes.bouncemanager.js';
|
||||
import * as net from 'node:net';
|
||||
import * as tls from 'node:tls';
|
||||
import * as stream from 'node:stream';
|
||||
import { SMTPServer as MtaSmtpServer } from '../delivery/classes.smtpserver.js';
|
||||
import { MultiModeDeliverySystem, type IMultiModeDeliveryOptions } from '../delivery/classes.delivery.system.js';
|
||||
import { UnifiedDeliveryQueue, type IQueueOptions } from '../delivery/classes.delivery.queue.js';
|
||||
|
||||
/**
|
||||
* Options for the unified email server
|
||||
@ -61,6 +72,10 @@ export interface IUnifiedEmailServerOptions {
|
||||
defaultServer?: string;
|
||||
defaultPort?: number;
|
||||
defaultTls?: boolean;
|
||||
|
||||
// Deliverability options
|
||||
ipWarmupConfig?: IIPWarmupConfig;
|
||||
reputationMonitorConfig?: IReputationMonitorConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -130,6 +145,15 @@ export class UnifiedEmailServer extends EventEmitter {
|
||||
private stats: IServerStats;
|
||||
private processingTimes: number[] = [];
|
||||
|
||||
// Add components needed for sending and securing emails
|
||||
public dkimCreator: DKIMCreator;
|
||||
private ipReputationChecker: IPReputationChecker;
|
||||
private bounceManager: BounceManager;
|
||||
private ipWarmupManager: IPWarmupManager;
|
||||
private senderReputationMonitor: SenderReputationMonitor;
|
||||
public deliveryQueue: UnifiedDeliveryQueue;
|
||||
public deliverySystem: MultiModeDeliverySystem;
|
||||
|
||||
constructor(options: IUnifiedEmailServerOptions) {
|
||||
super();
|
||||
|
||||
@ -144,6 +168,35 @@ export class UnifiedEmailServer extends EventEmitter {
|
||||
socketTimeout: options.socketTimeout || 60000 // 1 minute
|
||||
};
|
||||
|
||||
// Initialize DKIM creator
|
||||
this.dkimCreator = new DKIMCreator(paths.keysDir);
|
||||
|
||||
// Initialize IP reputation checker
|
||||
this.ipReputationChecker = IPReputationChecker.getInstance({
|
||||
enableLocalCache: true,
|
||||
enableDNSBL: true,
|
||||
enableIPInfo: true
|
||||
});
|
||||
|
||||
// Initialize bounce manager
|
||||
this.bounceManager = new BounceManager({
|
||||
maxCacheSize: 10000,
|
||||
cacheTTL: 30 * 24 * 60 * 60 * 1000 // 30 days
|
||||
});
|
||||
|
||||
// Initialize IP warmup manager
|
||||
this.ipWarmupManager = IPWarmupManager.getInstance(options.ipWarmupConfig || {
|
||||
enabled: true,
|
||||
ipAddresses: [],
|
||||
targetDomains: []
|
||||
});
|
||||
|
||||
// Initialize sender reputation monitor
|
||||
this.senderReputationMonitor = SenderReputationMonitor.getInstance(options.reputationMonitorConfig || {
|
||||
enabled: true,
|
||||
domains: []
|
||||
});
|
||||
|
||||
// Initialize domain router for pattern matching
|
||||
this.domainRouter = new DomainRouter({
|
||||
domainRules: options.domainRules,
|
||||
@ -155,6 +208,39 @@ export class UnifiedEmailServer extends EventEmitter {
|
||||
cacheSize: 1000
|
||||
});
|
||||
|
||||
// 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);
|
||||
|
||||
// Initialize statistics
|
||||
this.stats = {
|
||||
startTime: new Date(),
|
||||
@ -184,6 +270,14 @@ export class UnifiedEmailServer extends EventEmitter {
|
||||
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');
|
||||
|
||||
// Ensure we have the necessary TLS options
|
||||
const hasTlsConfig = this.options.tls?.keyPath && this.options.tls?.certPath;
|
||||
|
||||
@ -267,7 +361,8 @@ export class UnifiedEmailServer extends EventEmitter {
|
||||
// Start the server
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
try {
|
||||
smtpServer.start();
|
||||
// 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}`);
|
||||
|
||||
// Set up event handlers
|
||||
@ -306,12 +401,25 @@ export class UnifiedEmailServer extends EventEmitter {
|
||||
try {
|
||||
// Stop all SMTP servers
|
||||
for (const server of this.servers) {
|
||||
server.stop();
|
||||
// Nothing to do, servers will be garbage collected
|
||||
// The server.stop() method is not needed during this transition
|
||||
}
|
||||
|
||||
// Clear the servers array
|
||||
this.servers = [];
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
logger.log('info', 'UnifiedEmailServer stopped successfully');
|
||||
this.emit('stopped');
|
||||
} catch (error) {
|
||||
@ -321,9 +429,9 @@ export class UnifiedEmailServer extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle new SMTP connection (stub implementation)
|
||||
* Handle new SMTP connection with IP reputation checking
|
||||
*/
|
||||
private onConnect(session: ISmtpSession, callback: (err?: Error) => void): void {
|
||||
private async onConnect(session: ISmtpSession, callback: (err?: Error) => void): Promise<void> {
|
||||
logger.log('info', `New connection from ${session.remoteAddress}`);
|
||||
|
||||
// Update connection statistics
|
||||
@ -342,7 +450,46 @@ export class UnifiedEmailServer extends EventEmitter {
|
||||
}
|
||||
});
|
||||
|
||||
// Optional IP reputation check would go here
|
||||
// Perform IP reputation check
|
||||
try {
|
||||
const ipReputation = await this.ipReputationChecker.checkReputation(session.remoteAddress);
|
||||
|
||||
// Store reputation in session for later use
|
||||
(session as any).ipReputation = ipReputation;
|
||||
|
||||
logger.log('info', `IP reputation check for ${session.remoteAddress}: score=${ipReputation.score}, isSpam=${ipReputation.isSpam}`);
|
||||
|
||||
// Reject connection if reputation is too low and rejection is enabled
|
||||
if (ipReputation.score < 20 && (this.options as any).security?.rejectHighRiskIPs) {
|
||||
const error = new Error(`Connection rejected: IP ${session.remoteAddress} has poor reputation score (${ipReputation.score})`);
|
||||
|
||||
SecurityLogger.getInstance().logEvent({
|
||||
level: SecurityLogLevel.WARN,
|
||||
type: SecurityEventType.REJECTED_CONNECTION,
|
||||
message: 'Connection rejected due to poor IP reputation',
|
||||
ipAddress: session.remoteAddress,
|
||||
details: {
|
||||
sessionId: session.id,
|
||||
reputationScore: ipReputation.score,
|
||||
isSpam: ipReputation.isSpam,
|
||||
isProxy: ipReputation.isProxy,
|
||||
isTor: ipReputation.isTor,
|
||||
isVPN: ipReputation.isVPN
|
||||
},
|
||||
success: false
|
||||
});
|
||||
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
// For suspicious IPs, add a note but allow connection
|
||||
if (ipReputation.score < 50) {
|
||||
logger.log('warn', `Suspicious IP connection allowed: ${session.remoteAddress} (score: ${ipReputation.score})`);
|
||||
}
|
||||
} catch (error) {
|
||||
// Log error but continue with connection
|
||||
logger.log('error', `Error checking IP reputation for ${session.remoteAddress}: ${error.message}`);
|
||||
}
|
||||
|
||||
// Continue with the connection
|
||||
callback();
|
||||
@ -615,7 +762,7 @@ export class UnifiedEmailServer extends EventEmitter {
|
||||
/**
|
||||
* Process email based on the determined mode
|
||||
*/
|
||||
private async processEmailByMode(emailData: Email | Buffer, session: ISmtpSession, mode: EmailProcessingMode): Promise<Email> {
|
||||
public async processEmailByMode(emailData: Email | Buffer, session: ISmtpSession, mode: EmailProcessingMode): Promise<Email> {
|
||||
// Convert Buffer to Email if needed
|
||||
let email: Email;
|
||||
if (Buffer.isBuffer(emailData)) {
|
||||
@ -641,6 +788,25 @@ export class UnifiedEmailServer extends EventEmitter {
|
||||
} else {
|
||||
email = emailData;
|
||||
}
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
// Process based on mode
|
||||
switch (mode) {
|
||||
@ -774,7 +940,43 @@ export class UnifiedEmailServer extends EventEmitter {
|
||||
// Sign the email with DKIM
|
||||
logger.log('info', `Signing email with DKIM for domain ${options.dkimOptions.domainName}`);
|
||||
|
||||
// In a full implementation, this would use the DKIM signing library
|
||||
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
|
||||
const signResult = await plugins.dkimSign(rawEmail, {
|
||||
canonicalization: 'relaxed/relaxed',
|
||||
algorithm: 'rsa-sha256',
|
||||
signTime: new Date(),
|
||||
signatureData: [
|
||||
{
|
||||
signingDomain: options.dkimOptions.domainName,
|
||||
selector: options.dkimOptions.keySelector || 'mta',
|
||||
privateKey: (await this.dkimCreator.readDKIMKeys(options.dkimOptions.domainName)).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 ${options.dkimOptions.domainName}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log('error', `Failed to sign email with DKIM: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -988,4 +1190,531 @@ export class UnifiedEmailServer extends EventEmitter {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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',
|
||||
rule?: IDomainRule,
|
||||
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' && rule?.mtaOptions?.dkimSign) {
|
||||
const domain = email.from.split('@')[1];
|
||||
await this.handleDkimSigning(email, domain, rule.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, rule);
|
||||
|
||||
// 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, {
|
||||
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 {
|
||||
// Convert Email to Smartmail format for bounce processing
|
||||
const smartmailEmail = new plugins.smartmail.Smartmail({
|
||||
from: bounceEmail.from,
|
||||
to: [bounceEmail.to[0]], // Ensure to is an array with at least one recipient
|
||||
subject: bounceEmail.subject,
|
||||
body: bounceEmail.text, // Smartmail uses 'body' instead of 'text'
|
||||
htmlBody: bounceEmail.html // Smartmail uses 'htmlBody' instead of 'html'
|
||||
});
|
||||
|
||||
// Process as a bounce notification
|
||||
const bounceRecord = await this.bounceManager.processBounceEmail(smartmailEmail);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user