@push.rocks/smartmta 5.1.3 → 5.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/changelog.md +15 -0
  2. package/dist_ts/00_commitinfo_data.d.ts +8 -0
  3. package/dist_ts/00_commitinfo_data.js +9 -0
  4. package/dist_ts/index.d.ts +3 -0
  5. package/dist_ts/index.js +4 -0
  6. package/dist_ts/logger.d.ts +17 -0
  7. package/dist_ts/logger.js +76 -0
  8. package/dist_ts/mail/core/classes.bouncemanager.d.ts +185 -0
  9. package/dist_ts/mail/core/classes.bouncemanager.js +569 -0
  10. package/dist_ts/mail/core/classes.email.d.ts +291 -0
  11. package/dist_ts/mail/core/classes.email.js +802 -0
  12. package/dist_ts/mail/core/classes.emailvalidator.d.ts +61 -0
  13. package/dist_ts/mail/core/classes.emailvalidator.js +184 -0
  14. package/dist_ts/mail/core/classes.templatemanager.d.ts +95 -0
  15. package/dist_ts/mail/core/classes.templatemanager.js +240 -0
  16. package/dist_ts/mail/core/index.d.ts +4 -0
  17. package/dist_ts/mail/core/index.js +6 -0
  18. package/dist_ts/mail/delivery/classes.delivery.queue.d.ts +163 -0
  19. package/dist_ts/mail/delivery/classes.delivery.queue.js +488 -0
  20. package/dist_ts/mail/delivery/classes.delivery.system.d.ts +160 -0
  21. package/dist_ts/mail/delivery/classes.delivery.system.js +630 -0
  22. package/dist_ts/mail/delivery/classes.unified.rate.limiter.d.ts +200 -0
  23. package/dist_ts/mail/delivery/classes.unified.rate.limiter.js +820 -0
  24. package/dist_ts/mail/delivery/index.d.ts +4 -0
  25. package/dist_ts/mail/delivery/index.js +6 -0
  26. package/dist_ts/mail/delivery/interfaces.d.ts +140 -0
  27. package/dist_ts/mail/delivery/interfaces.js +17 -0
  28. package/dist_ts/mail/index.d.ts +7 -0
  29. package/dist_ts/mail/index.js +12 -0
  30. package/dist_ts/mail/routing/classes.dkim.manager.d.ts +25 -0
  31. package/dist_ts/mail/routing/classes.dkim.manager.js +127 -0
  32. package/dist_ts/mail/routing/classes.dns.manager.d.ts +79 -0
  33. package/dist_ts/mail/routing/classes.dns.manager.js +415 -0
  34. package/dist_ts/mail/routing/classes.domain.registry.d.ts +54 -0
  35. package/dist_ts/mail/routing/classes.domain.registry.js +119 -0
  36. package/dist_ts/mail/routing/classes.email.action.executor.d.ts +33 -0
  37. package/dist_ts/mail/routing/classes.email.action.executor.js +137 -0
  38. package/dist_ts/mail/routing/classes.email.router.d.ts +171 -0
  39. package/dist_ts/mail/routing/classes.email.router.js +494 -0
  40. package/dist_ts/mail/routing/classes.unified.email.server.d.ts +241 -0
  41. package/dist_ts/mail/routing/classes.unified.email.server.js +935 -0
  42. package/dist_ts/mail/routing/index.d.ts +7 -0
  43. package/dist_ts/mail/routing/index.js +9 -0
  44. package/dist_ts/mail/routing/interfaces.d.ts +187 -0
  45. package/dist_ts/mail/routing/interfaces.js +2 -0
  46. package/dist_ts/mail/security/classes.dkimcreator.d.ts +72 -0
  47. package/dist_ts/mail/security/classes.dkimcreator.js +360 -0
  48. package/dist_ts/mail/security/classes.spfverifier.d.ts +62 -0
  49. package/dist_ts/mail/security/classes.spfverifier.js +87 -0
  50. package/dist_ts/mail/security/index.d.ts +2 -0
  51. package/dist_ts/mail/security/index.js +4 -0
  52. package/dist_ts/paths.d.ts +14 -0
  53. package/dist_ts/paths.js +39 -0
  54. package/dist_ts/plugins.d.ts +24 -0
  55. package/dist_ts/plugins.js +28 -0
  56. package/dist_ts/security/classes.contentscanner.d.ts +130 -0
  57. package/dist_ts/security/classes.contentscanner.js +338 -0
  58. package/dist_ts/security/classes.ipreputationchecker.d.ts +73 -0
  59. package/dist_ts/security/classes.ipreputationchecker.js +263 -0
  60. package/dist_ts/security/classes.rustsecuritybridge.d.ts +403 -0
  61. package/dist_ts/security/classes.rustsecuritybridge.js +502 -0
  62. package/dist_ts/security/classes.securitylogger.d.ts +140 -0
  63. package/dist_ts/security/classes.securitylogger.js +235 -0
  64. package/dist_ts/security/index.d.ts +4 -0
  65. package/dist_ts/security/index.js +5 -0
  66. package/package.json +6 -1
  67. package/ts/00_commitinfo_data.ts +8 -0
  68. package/ts/index.ts +3 -0
  69. package/ts/logger.ts +91 -0
  70. package/ts/mail/core/classes.bouncemanager.ts +731 -0
  71. package/ts/mail/core/classes.email.ts +942 -0
  72. package/ts/mail/core/classes.emailvalidator.ts +239 -0
  73. package/ts/mail/core/classes.templatemanager.ts +320 -0
  74. package/ts/mail/core/index.ts +5 -0
  75. package/ts/mail/delivery/classes.delivery.queue.ts +645 -0
  76. package/ts/mail/delivery/classes.delivery.system.ts +816 -0
  77. package/ts/mail/delivery/classes.unified.rate.limiter.ts +1053 -0
  78. package/ts/mail/delivery/index.ts +5 -0
  79. package/ts/mail/delivery/interfaces.ts +167 -0
  80. package/ts/mail/index.ts +17 -0
  81. package/ts/mail/routing/classes.dkim.manager.ts +157 -0
  82. package/ts/mail/routing/classes.dns.manager.ts +573 -0
  83. package/ts/mail/routing/classes.domain.registry.ts +139 -0
  84. package/ts/mail/routing/classes.email.action.executor.ts +175 -0
  85. package/ts/mail/routing/classes.email.router.ts +575 -0
  86. package/ts/mail/routing/classes.unified.email.server.ts +1207 -0
  87. package/ts/mail/routing/index.ts +9 -0
  88. package/ts/mail/routing/interfaces.ts +202 -0
  89. package/ts/mail/security/classes.dkimcreator.ts +447 -0
  90. package/ts/mail/security/classes.spfverifier.ts +126 -0
  91. package/ts/mail/security/index.ts +3 -0
  92. package/ts/paths.ts +48 -0
  93. package/ts/plugins.ts +53 -0
  94. package/ts/security/classes.contentscanner.ts +400 -0
  95. package/ts/security/classes.ipreputationchecker.ts +315 -0
  96. package/ts/security/classes.rustsecuritybridge.ts +964 -0
  97. package/ts/security/classes.securitylogger.ts +299 -0
  98. package/ts/security/index.ts +40 -0
@@ -0,0 +1,1207 @@
1
+ import * as plugins from '../../plugins.js';
2
+ import * as paths from '../../paths.js';
3
+ import { EventEmitter } from 'events';
4
+ import { logger } from '../../logger.js';
5
+ import {
6
+ SecurityLogger,
7
+ SecurityLogLevel,
8
+ SecurityEventType
9
+ } from '../../security/index.js';
10
+ import { DKIMCreator } from '../security/classes.dkimcreator.js';
11
+ import { RustSecurityBridge } from '../../security/classes.rustsecuritybridge.js';
12
+ import type { IEmailReceivedEvent, IAuthRequestEvent, IEmailData } from '../../security/classes.rustsecuritybridge.js';
13
+ import { EmailRouter } from './classes.email.router.js';
14
+ import type { IEmailRoute, IEmailAction, IEmailContext, IEmailDomainConfig } from './interfaces.js';
15
+ import { Email } from '../core/classes.email.js';
16
+ import { DomainRegistry } from './classes.domain.registry.js';
17
+ import { DnsManager } from './classes.dns.manager.js';
18
+ import { BounceManager, BounceType, BounceCategory } from '../core/classes.bouncemanager.js';
19
+ import type { ISmtpSendResult, IOutboundEmail } from '../../security/classes.rustsecuritybridge.js';
20
+ import { MultiModeDeliverySystem, type IMultiModeDeliveryOptions } from '../delivery/classes.delivery.system.js';
21
+ import { UnifiedDeliveryQueue, type IQueueOptions } from '../delivery/classes.delivery.queue.js';
22
+ import { UnifiedRateLimiter, type IHierarchicalRateLimits } from '../delivery/classes.unified.rate.limiter.js';
23
+ import { SmtpState } from '../delivery/interfaces.js';
24
+ import type { EmailProcessingMode, ISmtpSession as IBaseSmtpSession } from '../delivery/interfaces.js';
25
+ import { EmailActionExecutor } from './classes.email.action.executor.js';
26
+ import { DkimManager } from './classes.dkim.manager.js';
27
+
28
+
29
+ /** External DcRouter interface shape used by UnifiedEmailServer */
30
+ interface DcRouter {
31
+ storageManager: any;
32
+ dnsServer?: any;
33
+ options?: any;
34
+ }
35
+
36
+ /**
37
+ * Extended SMTP session interface with route information
38
+ */
39
+ export interface IExtendedSmtpSession extends ISmtpSession {
40
+ /**
41
+ * Matched route for this session
42
+ */
43
+ matchedRoute?: IEmailRoute;
44
+ }
45
+
46
+ /**
47
+ * Options for the unified email server
48
+ */
49
+ export interface IUnifiedEmailServerOptions {
50
+ // Base server options
51
+ ports: number[];
52
+ hostname: string;
53
+ domains: IEmailDomainConfig[]; // Domain configurations
54
+ banner?: string;
55
+ debug?: boolean;
56
+ useSocketHandler?: boolean; // Use socket-handler mode instead of port listening
57
+
58
+ // Authentication options
59
+ auth?: {
60
+ required?: boolean;
61
+ methods?: ('PLAIN' | 'LOGIN' | 'OAUTH2')[];
62
+ users?: Array<{username: string, password: string}>;
63
+ };
64
+
65
+ // TLS options
66
+ tls?: {
67
+ certPath?: string;
68
+ keyPath?: string;
69
+ caPath?: string;
70
+ minVersion?: string;
71
+ ciphers?: string;
72
+ };
73
+
74
+ // Limits
75
+ maxMessageSize?: number;
76
+ maxClients?: number;
77
+ maxConnections?: number;
78
+
79
+ // Connection options
80
+ connectionTimeout?: number;
81
+ socketTimeout?: number;
82
+
83
+ // Email routing rules
84
+ routes: IEmailRoute[];
85
+
86
+ // Global defaults for all domains
87
+ defaults?: {
88
+ dnsMode?: 'forward' | 'internal-dns' | 'external-dns';
89
+ dkim?: IEmailDomainConfig['dkim'];
90
+ rateLimits?: IEmailDomainConfig['rateLimits'];
91
+ };
92
+
93
+ // Outbound settings
94
+ outbound?: {
95
+ maxConnections?: number;
96
+ connectionTimeout?: number;
97
+ socketTimeout?: number;
98
+ retryAttempts?: number;
99
+ defaultFrom?: string;
100
+ };
101
+
102
+ // Rate limiting (global limits, can be overridden per domain)
103
+ rateLimits?: IHierarchicalRateLimits;
104
+ }
105
+
106
+
107
+ /**
108
+ * Extended SMTP session interface for UnifiedEmailServer
109
+ */
110
+ export interface ISmtpSession extends IBaseSmtpSession {
111
+ /**
112
+ * User information if authenticated
113
+ */
114
+ user?: {
115
+ username: string;
116
+ [key: string]: any;
117
+ };
118
+
119
+ /**
120
+ * Matched route for this session
121
+ */
122
+ matchedRoute?: IEmailRoute;
123
+ }
124
+
125
+ /**
126
+ * Authentication data for SMTP
127
+ */
128
+ import type { ISmtpAuth } from '../delivery/interfaces.js';
129
+ export type IAuthData = ISmtpAuth;
130
+
131
+ /**
132
+ * Server statistics
133
+ */
134
+ export interface IServerStats {
135
+ startTime: Date;
136
+ connections: {
137
+ current: number;
138
+ total: number;
139
+ };
140
+ messages: {
141
+ processed: number;
142
+ delivered: number;
143
+ failed: number;
144
+ };
145
+ processingTime: {
146
+ avg: number;
147
+ max: number;
148
+ min: number;
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Unified email server that handles all email traffic with pattern-based routing
154
+ */
155
+ export class UnifiedEmailServer extends EventEmitter {
156
+ private dcRouter: DcRouter;
157
+ private options: IUnifiedEmailServerOptions;
158
+ private emailRouter: EmailRouter;
159
+ public domainRegistry: DomainRegistry;
160
+ private servers: any[] = [];
161
+ private stats: IServerStats;
162
+
163
+ // Add components needed for sending and securing emails
164
+ public dkimCreator: DKIMCreator;
165
+ private rustBridge: RustSecurityBridge;
166
+ private bounceManager: BounceManager;
167
+ public deliveryQueue: UnifiedDeliveryQueue;
168
+ public deliverySystem: MultiModeDeliverySystem;
169
+ private rateLimiter: UnifiedRateLimiter; // TODO: Implement rate limiting in SMTP server handlers
170
+
171
+ // Extracted subsystems
172
+ private actionExecutor: EmailActionExecutor;
173
+ private dkimManager: DkimManager;
174
+
175
+ constructor(dcRouter: DcRouter, options: IUnifiedEmailServerOptions) {
176
+ super();
177
+ this.dcRouter = dcRouter;
178
+
179
+ // Set default options
180
+ this.options = {
181
+ ...options,
182
+ banner: options.banner || `${options.hostname} ESMTP UnifiedEmailServer`,
183
+ maxMessageSize: options.maxMessageSize || 10 * 1024 * 1024, // 10MB
184
+ maxClients: options.maxClients || 100,
185
+ maxConnections: options.maxConnections || 1000,
186
+ connectionTimeout: options.connectionTimeout || 60000, // 1 minute
187
+ socketTimeout: options.socketTimeout || 60000 // 1 minute
188
+ };
189
+
190
+ // Initialize Rust security bridge (singleton)
191
+ this.rustBridge = RustSecurityBridge.getInstance();
192
+
193
+ // Initialize DKIM creator with storage manager
194
+ this.dkimCreator = new DKIMCreator(paths.keysDir, dcRouter.storageManager);
195
+
196
+ // Initialize bounce manager with storage manager
197
+ this.bounceManager = new BounceManager({
198
+ maxCacheSize: 10000,
199
+ cacheTTL: 30 * 24 * 60 * 60 * 1000, // 30 days
200
+ storageManager: dcRouter.storageManager
201
+ });
202
+
203
+ // Initialize domain registry
204
+ this.domainRegistry = new DomainRegistry(options.domains, options.defaults);
205
+
206
+ // Initialize email router with routes and storage manager
207
+ this.emailRouter = new EmailRouter(options.routes || [], {
208
+ storageManager: dcRouter.storageManager,
209
+ persistChanges: true
210
+ });
211
+
212
+ // Initialize rate limiter
213
+ this.rateLimiter = new UnifiedRateLimiter(options.rateLimits || {
214
+ global: {
215
+ maxConnectionsPerIP: 10,
216
+ maxMessagesPerMinute: 100,
217
+ maxRecipientsPerMessage: 50,
218
+ maxErrorsPerIP: 10,
219
+ maxAuthFailuresPerIP: 5,
220
+ blockDuration: 300000 // 5 minutes
221
+ }
222
+ });
223
+
224
+ // Initialize delivery components
225
+ const queueOptions: IQueueOptions = {
226
+ storageType: 'memory', // Default to memory storage
227
+ maxRetries: 3,
228
+ baseRetryDelay: 300000, // 5 minutes
229
+ maxRetryDelay: 3600000 // 1 hour
230
+ };
231
+
232
+ this.deliveryQueue = new UnifiedDeliveryQueue(queueOptions);
233
+
234
+ const deliveryOptions: IMultiModeDeliveryOptions = {
235
+ globalRateLimit: 100, // Default to 100 emails per minute
236
+ concurrentDeliveries: 10,
237
+ processBounces: true,
238
+ bounceHandler: {
239
+ processSmtpFailure: this.processSmtpFailure.bind(this)
240
+ },
241
+ onDeliverySuccess: async (_item, _result) => {
242
+ // Delivery success recorded via delivery system
243
+ }
244
+ };
245
+
246
+ this.deliverySystem = new MultiModeDeliverySystem(this.deliveryQueue, deliveryOptions, this);
247
+
248
+ // Initialize action executor
249
+ this.actionExecutor = new EmailActionExecutor({
250
+ sendOutboundEmail: this.sendOutboundEmail.bind(this),
251
+ bounceManager: this.bounceManager,
252
+ deliveryQueue: this.deliveryQueue,
253
+ });
254
+
255
+ // Initialize DKIM manager
256
+ this.dkimManager = new DkimManager(this.dkimCreator, this.domainRegistry, dcRouter, this.rustBridge);
257
+
258
+ // Initialize statistics
259
+ this.stats = {
260
+ startTime: new Date(),
261
+ connections: {
262
+ current: 0,
263
+ total: 0
264
+ },
265
+ messages: {
266
+ processed: 0,
267
+ delivered: 0,
268
+ failed: 0
269
+ },
270
+ processingTime: {
271
+ avg: 0,
272
+ max: 0,
273
+ min: 0
274
+ }
275
+ };
276
+
277
+ // We'll create the SMTP servers during the start() method
278
+ }
279
+
280
+ /**
281
+ * Send an outbound email via the Rust SMTP client.
282
+ * Uses connection pooling in the Rust binary for efficiency.
283
+ */
284
+ public async sendOutboundEmail(host: string, port: number, email: Email, options?: {
285
+ auth?: { user: string; pass: string };
286
+ dkimDomain?: string;
287
+ dkimSelector?: string;
288
+ tlsOpportunistic?: boolean;
289
+ }): Promise<ISmtpSendResult> {
290
+ // Build DKIM config if domain has keys
291
+ let dkim: { domain: string; selector: string; privateKey: string } | undefined;
292
+ if (options?.dkimDomain) {
293
+ try {
294
+ const { privateKey } = await this.dkimCreator.readDKIMKeys(options.dkimDomain);
295
+ dkim = { domain: options.dkimDomain, selector: options.dkimSelector || 'default', privateKey };
296
+ } catch (err) {
297
+ logger.log('warn', `Failed to read DKIM keys for ${options.dkimDomain}: ${(err as Error).message}`);
298
+ }
299
+ }
300
+
301
+ // Serialize the Email to the outbound format
302
+ const outboundEmail: IOutboundEmail = {
303
+ from: email.from,
304
+ to: email.to,
305
+ cc: email.cc || [],
306
+ bcc: email.bcc || [],
307
+ subject: email.subject || '',
308
+ text: email.text || '',
309
+ html: email.html || undefined,
310
+ headers: email.headers as Record<string, string> || {},
311
+ };
312
+
313
+ return this.rustBridge.sendOutboundEmail({
314
+ host,
315
+ port,
316
+ secure: port === 465,
317
+ domain: this.options.hostname,
318
+ auth: options?.auth,
319
+ email: outboundEmail,
320
+ dkim,
321
+ connectionTimeoutSecs: Math.floor((this.options.outbound?.connectionTimeout || 30000) / 1000),
322
+ socketTimeoutSecs: Math.floor((this.options.outbound?.socketTimeout || 120000) / 1000),
323
+ poolKey: `${host}:${port}`,
324
+ maxPoolConnections: this.options.outbound?.maxConnections || 10,
325
+ tlsOpportunistic: options?.tlsOpportunistic ?? (port === 25),
326
+ });
327
+ }
328
+
329
+ /**
330
+ * Start the unified email server
331
+ */
332
+ public async start(): Promise<void> {
333
+ logger.log('info', `Starting UnifiedEmailServer on ports: ${(this.options.ports as number[]).join(', ')}`);
334
+
335
+ try {
336
+ await this.startDeliveryPipeline();
337
+ await this.startRustBridge();
338
+ await this.initializeDkimAndDns();
339
+ this.registerBridgeEventHandlers();
340
+ await this.startSmtpServer();
341
+ logger.log('info', 'UnifiedEmailServer started successfully');
342
+ this.emit('started');
343
+ } catch (error) {
344
+ logger.log('error', `Failed to start UnifiedEmailServer: ${error.message}`);
345
+ throw error;
346
+ }
347
+ }
348
+
349
+ private async startDeliveryPipeline(): Promise<void> {
350
+ await this.deliveryQueue.initialize();
351
+ logger.log('info', 'Email delivery queue initialized');
352
+
353
+ await this.deliverySystem.start();
354
+ logger.log('info', 'Email delivery system started');
355
+ }
356
+
357
+ private async startRustBridge(): Promise<void> {
358
+ const bridgeOk = await this.rustBridge.start();
359
+ if (!bridgeOk) {
360
+ throw new Error('Rust security bridge failed to start. The mailer-bin binary is required. Run "pnpm build" to compile it.');
361
+ }
362
+ logger.log('info', 'Rust security bridge started — Rust is the primary security backend');
363
+
364
+ this.rustBridge.on('stateChange', ({ oldState, newState }: { oldState: string; newState: string }) => {
365
+ if (newState === 'failed') this.emit('bridgeFailed');
366
+ else if (newState === 'restarting') this.emit('bridgeRestarting');
367
+ else if (newState === 'running' && oldState === 'restarting') this.emit('bridgeRecovered');
368
+ });
369
+ }
370
+
371
+ private async initializeDkimAndDns(): Promise<void> {
372
+ await this.dkimManager.setupDkimForDomains();
373
+ logger.log('info', 'DKIM configuration completed for all domains');
374
+
375
+ const dnsManager = new DnsManager(this.dcRouter);
376
+ await dnsManager.ensureDnsRecords(this.domainRegistry.getAllConfigs(), this.dkimCreator);
377
+ logger.log('info', 'DNS records ensured for all configured domains');
378
+
379
+ this.applyDomainRateLimits();
380
+ logger.log('info', 'Per-domain rate limits configured');
381
+
382
+ await this.dkimManager.checkAndRotateDkimKeys();
383
+ logger.log('info', 'DKIM key rotation check completed');
384
+ }
385
+
386
+ private registerBridgeEventHandlers(): void {
387
+ this.rustBridge.onEmailReceived(async (data) => {
388
+ try {
389
+ await this.handleRustEmailReceived(data);
390
+ } catch (err) {
391
+ logger.log('error', `Error handling email from Rust SMTP: ${(err as Error).message}`);
392
+ try {
393
+ await this.rustBridge.sendEmailProcessingResult({
394
+ correlationId: data.correlationId,
395
+ accepted: false,
396
+ smtpCode: 451,
397
+ smtpMessage: 'Internal processing error',
398
+ });
399
+ } catch (sendErr) {
400
+ logger.log('warn', `Could not send rejection back to Rust: ${(sendErr as Error).message}`);
401
+ }
402
+ }
403
+ });
404
+
405
+ this.rustBridge.onAuthRequest(async (data) => {
406
+ try {
407
+ await this.handleRustAuthRequest(data);
408
+ } catch (err) {
409
+ logger.log('error', `Error handling auth from Rust SMTP: ${(err as Error).message}`);
410
+ try {
411
+ await this.rustBridge.sendAuthResult({
412
+ correlationId: data.correlationId,
413
+ success: false,
414
+ message: 'Internal auth error',
415
+ });
416
+ } catch (sendErr) {
417
+ logger.log('warn', `Could not send auth rejection back to Rust: ${(sendErr as Error).message}`);
418
+ }
419
+ }
420
+ });
421
+
422
+ this.rustBridge.onScramCredentialRequest(async (data) => {
423
+ try {
424
+ await this.handleScramCredentialRequest(data);
425
+ } catch (err) {
426
+ logger.log('error', `Error handling SCRAM credential request: ${(err as Error).message}`);
427
+ try {
428
+ await this.rustBridge.sendScramCredentialResult({
429
+ correlationId: data.correlationId,
430
+ found: false,
431
+ });
432
+ } catch (sendErr) {
433
+ logger.log('warn', `Could not send SCRAM credential rejection: ${(sendErr as Error).message}`);
434
+ }
435
+ }
436
+ });
437
+ }
438
+
439
+ private async startSmtpServer(): Promise<void> {
440
+ const hasTlsConfig = this.options.tls?.keyPath && this.options.tls?.certPath;
441
+ let tlsCertPem: string | undefined;
442
+ let tlsKeyPem: string | undefined;
443
+
444
+ if (hasTlsConfig) {
445
+ try {
446
+ tlsKeyPem = plugins.fs.readFileSync(this.options.tls.keyPath!, 'utf8');
447
+ tlsCertPem = plugins.fs.readFileSync(this.options.tls.certPath!, 'utf8');
448
+ logger.log('info', 'TLS certificates loaded successfully');
449
+ } catch (error) {
450
+ logger.log('warn', `Failed to load TLS certificates: ${error.message}`);
451
+ }
452
+ }
453
+
454
+ const smtpPorts = (this.options.ports as number[]).filter(p => p !== 465);
455
+ const securePort = (this.options.ports as number[]).find(p => p === 465);
456
+
457
+ const started = await this.rustBridge.startSmtpServer({
458
+ hostname: this.options.hostname,
459
+ ports: smtpPorts,
460
+ securePort: securePort,
461
+ tlsCertPem,
462
+ tlsKeyPem,
463
+ maxMessageSize: this.options.maxMessageSize || 10 * 1024 * 1024,
464
+ maxConnections: this.options.maxConnections || this.options.maxClients || 100,
465
+ maxRecipients: 100,
466
+ connectionTimeoutSecs: this.options.connectionTimeout ? Math.floor(this.options.connectionTimeout / 1000) : 30,
467
+ dataTimeoutSecs: 60,
468
+ authEnabled: !!this.options.auth?.required || !!(this.options.auth?.users?.length),
469
+ maxAuthFailures: 3,
470
+ socketTimeoutSecs: this.options.socketTimeout ? Math.floor(this.options.socketTimeout / 1000) : 300,
471
+ processingTimeoutSecs: 30,
472
+ rateLimits: this.options.rateLimits ? {
473
+ maxConnectionsPerIp: this.options.rateLimits.global?.maxConnectionsPerIP || 50,
474
+ maxMessagesPerSender: this.options.rateLimits.global?.maxMessagesPerMinute || 100,
475
+ maxAuthFailuresPerIp: this.options.rateLimits.global?.maxAuthFailuresPerIP || 5,
476
+ windowSecs: 60,
477
+ } : undefined,
478
+ });
479
+
480
+ if (!started) {
481
+ throw new Error('Failed to start Rust SMTP server');
482
+ }
483
+
484
+ logger.log('info', `Rust SMTP server listening on ports: ${smtpPorts.join(', ')}${securePort ? ` + ${securePort} (TLS)` : ''}`);
485
+ }
486
+
487
+ /**
488
+ * Stop the unified email server
489
+ */
490
+ public async stop(): Promise<void> {
491
+ logger.log('info', 'Stopping UnifiedEmailServer');
492
+
493
+ try {
494
+ // Stop the Rust SMTP server first
495
+ try {
496
+ await this.rustBridge.stopSmtpServer();
497
+ logger.log('info', 'Rust SMTP server stopped');
498
+ } catch (err) {
499
+ logger.log('warn', `Error stopping Rust SMTP server: ${(err as Error).message}`);
500
+ }
501
+
502
+ // Clear the servers array - servers will be garbage collected
503
+ this.servers = [];
504
+
505
+ // Remove bridge state change listener and stop bridge
506
+ this.rustBridge.removeAllListeners('stateChange');
507
+ await this.rustBridge.stop();
508
+
509
+ // Stop the delivery system
510
+ if (this.deliverySystem) {
511
+ await this.deliverySystem.stop();
512
+ logger.log('info', 'Email delivery system stopped');
513
+ }
514
+
515
+ // Shut down the delivery queue
516
+ if (this.deliveryQueue) {
517
+ await this.deliveryQueue.shutdown();
518
+ logger.log('info', 'Email delivery queue shut down');
519
+ }
520
+
521
+ // Close all Rust SMTP client connection pools
522
+ try {
523
+ await this.rustBridge.closeSmtpPool();
524
+ } catch {
525
+ // Bridge may already be stopped
526
+ }
527
+
528
+ logger.log('info', 'UnifiedEmailServer stopped successfully');
529
+ this.emit('stopped');
530
+ } catch (error) {
531
+ logger.log('error', `Error stopping UnifiedEmailServer: ${error.message}`);
532
+ throw error;
533
+ }
534
+ }
535
+
536
+ // -----------------------------------------------------------------------
537
+ // Rust SMTP server event handlers
538
+ // -----------------------------------------------------------------------
539
+
540
+ /**
541
+ * Handle an emailReceived event from the Rust SMTP server.
542
+ */
543
+ private async handleRustEmailReceived(data: IEmailReceivedEvent): Promise<void> {
544
+ const { correlationId, mailFrom, rcptTo, remoteAddr, clientHostname, secure, authenticatedUser } = data;
545
+
546
+ logger.log('info', `Rust SMTP received email from=${mailFrom} to=${rcptTo.join(',')} remote=${remoteAddr}`);
547
+
548
+ try {
549
+ // Decode the email data
550
+ let rawMessageBuffer: Buffer;
551
+ if (data.data.type === 'inline' && data.data.base64) {
552
+ rawMessageBuffer = Buffer.from(data.data.base64, 'base64');
553
+ } else if (data.data.type === 'file' && data.data.path) {
554
+ rawMessageBuffer = plugins.fs.readFileSync(data.data.path);
555
+ // Clean up temp file
556
+ try {
557
+ plugins.fs.unlinkSync(data.data.path);
558
+ } catch {
559
+ // Ignore cleanup errors
560
+ }
561
+ } else {
562
+ throw new Error('Invalid email data transport');
563
+ }
564
+
565
+ // Build a session-like object for processEmailByMode
566
+ const session: IExtendedSmtpSession = {
567
+ id: data.sessionId || 'rust-' + Math.random().toString(36).substring(2),
568
+ state: SmtpState.FINISHED,
569
+ mailFrom: mailFrom,
570
+ rcptTo: rcptTo,
571
+ emailData: rawMessageBuffer.toString('utf8'),
572
+ useTLS: secure,
573
+ connectionEnded: false,
574
+ remoteAddress: remoteAddr,
575
+ clientHostname: clientHostname || '',
576
+ secure: secure,
577
+ authenticated: !!authenticatedUser,
578
+ envelope: {
579
+ mailFrom: { address: mailFrom, args: {} },
580
+ rcptTo: rcptTo.map(addr => ({ address: addr, args: {} })),
581
+ },
582
+ };
583
+
584
+ if (authenticatedUser) {
585
+ session.user = { username: authenticatedUser };
586
+ }
587
+
588
+ // Attach pre-computed security results from Rust in-process pipeline
589
+ if (data.securityResults) {
590
+ (session as any)._precomputedSecurityResults = data.securityResults;
591
+ }
592
+
593
+ // Process the email through the routing system
594
+ await this.processEmailByMode(rawMessageBuffer, session);
595
+
596
+ // Send acceptance back to Rust
597
+ await this.rustBridge.sendEmailProcessingResult({
598
+ correlationId,
599
+ accepted: true,
600
+ smtpCode: 250,
601
+ smtpMessage: '2.0.0 Message accepted for delivery',
602
+ });
603
+ } catch (err) {
604
+ logger.log('error', `Failed to process email from Rust SMTP: ${(err as Error).message}`);
605
+ await this.rustBridge.sendEmailProcessingResult({
606
+ correlationId,
607
+ accepted: false,
608
+ smtpCode: 550,
609
+ smtpMessage: `5.0.0 Processing failed: ${(err as Error).message}`,
610
+ });
611
+ }
612
+ }
613
+
614
+ /**
615
+ * Handle an authRequest event from the Rust SMTP server.
616
+ */
617
+ private async handleRustAuthRequest(data: IAuthRequestEvent): Promise<void> {
618
+ const { correlationId, username, password, remoteAddr } = data;
619
+
620
+ logger.log('info', `Rust SMTP auth request for user=${username} from=${remoteAddr}`);
621
+
622
+ // Check against configured users
623
+ const users = this.options.auth?.users || [];
624
+ const matched = users.find(
625
+ u => u.username === username && u.password === password
626
+ );
627
+
628
+ if (matched) {
629
+ await this.rustBridge.sendAuthResult({
630
+ correlationId,
631
+ success: true,
632
+ });
633
+ } else {
634
+ logger.log('warn', `Auth failed for user=${username} from=${remoteAddr}`);
635
+ await this.rustBridge.sendAuthResult({
636
+ correlationId,
637
+ success: false,
638
+ message: 'Invalid credentials',
639
+ });
640
+ }
641
+ }
642
+
643
+ /**
644
+ * Handle a SCRAM credential request from the Rust SMTP server.
645
+ * Computes SCRAM-SHA-256 credentials from the stored password for the given user.
646
+ */
647
+ private async handleScramCredentialRequest(data: { correlationId: string; username: string; remoteAddr: string }): Promise<void> {
648
+ const { correlationId, username, remoteAddr } = data;
649
+ const crypto = await import('crypto');
650
+
651
+ logger.log('info', `SCRAM credential request for user=${username} from=${remoteAddr}`);
652
+
653
+ const users = this.options.auth?.users || [];
654
+ const matched = users.find(u => u.username === username);
655
+
656
+ if (!matched) {
657
+ await this.rustBridge.sendScramCredentialResult({
658
+ correlationId,
659
+ found: false,
660
+ });
661
+ return;
662
+ }
663
+
664
+ // Compute SCRAM-SHA-256 credentials from plaintext password
665
+ const salt = crypto.randomBytes(16);
666
+ const iterations = 4096;
667
+
668
+ // SaltedPassword = PBKDF2-HMAC-SHA256(password, salt, iterations, 32)
669
+ const saltedPassword = crypto.pbkdf2Sync(matched.password, salt, iterations, 32, 'sha256');
670
+
671
+ // ClientKey = HMAC-SHA256(SaltedPassword, "Client Key")
672
+ const clientKey = crypto.createHmac('sha256', saltedPassword).update('Client Key').digest();
673
+
674
+ // StoredKey = SHA256(ClientKey)
675
+ const storedKey = crypto.createHash('sha256').update(clientKey).digest();
676
+
677
+ // ServerKey = HMAC-SHA256(SaltedPassword, "Server Key")
678
+ const serverKey = crypto.createHmac('sha256', saltedPassword).update('Server Key').digest();
679
+
680
+ await this.rustBridge.sendScramCredentialResult({
681
+ correlationId,
682
+ found: true,
683
+ salt: salt.toString('base64'),
684
+ iterations,
685
+ storedKey: storedKey.toString('base64'),
686
+ serverKey: serverKey.toString('base64'),
687
+ });
688
+ }
689
+
690
+ /**
691
+ * Verify inbound email security (DKIM/SPF/DMARC) using pre-computed Rust results
692
+ * or falling back to IPC call if no pre-computed results are available.
693
+ */
694
+ private async verifyInboundSecurity(email: Email, session: IExtendedSmtpSession): Promise<void> {
695
+ try {
696
+ // Check for pre-computed results from Rust in-process security pipeline
697
+ const precomputed = (session as any)._precomputedSecurityResults;
698
+ let result: any;
699
+
700
+ if (precomputed) {
701
+ logger.log('info', 'Using pre-computed security results from Rust in-process pipeline');
702
+ result = precomputed;
703
+ } else {
704
+ // Fallback: IPC round-trip to Rust (for backward compat)
705
+ const rawMessage = session.emailData || email.toRFC822String();
706
+ result = await this.rustBridge.verifyEmail({
707
+ rawMessage,
708
+ ip: session.remoteAddress,
709
+ heloDomain: session.clientHostname || '',
710
+ hostname: this.options.hostname,
711
+ mailFrom: session.envelope?.mailFrom?.address || session.mailFrom || '',
712
+ });
713
+ }
714
+
715
+ // Apply DKIM result headers
716
+ if (result.dkim && result.dkim.length > 0) {
717
+ const dkimSummary = result.dkim
718
+ .map((d: any) => `${d.status}${d.domain ? ` (${d.domain})` : ''}`)
719
+ .join(', ');
720
+ email.addHeader('X-DKIM-Result', dkimSummary);
721
+ }
722
+
723
+ // Apply SPF result header
724
+ if (result.spf) {
725
+ email.addHeader('Received-SPF', `${result.spf.result} (domain: ${result.spf.domain}, ip: ${result.spf.ip})`);
726
+
727
+ // Mark as spam on SPF hard fail
728
+ if (result.spf.result === 'fail') {
729
+ email.mightBeSpam = true;
730
+ logger.log('warn', `SPF fail for ${session.remoteAddress} — marking as potential spam`);
731
+ }
732
+ }
733
+
734
+ // Apply DMARC result header and policy
735
+ if (result.dmarc) {
736
+ email.addHeader('X-DMARC-Result', `${result.dmarc.action} (policy=${result.dmarc.policy}, dkim=${result.dmarc.dkim_result}, spf=${result.dmarc.spf_result})`);
737
+
738
+ if (result.dmarc.action === 'reject') {
739
+ email.mightBeSpam = true;
740
+ logger.log('warn', `DMARC reject for domain ${result.dmarc.domain} — marking as spam`);
741
+ } else if (result.dmarc.action === 'quarantine') {
742
+ email.mightBeSpam = true;
743
+ logger.log('info', `DMARC quarantine for domain ${result.dmarc.domain} — marking as potential spam`);
744
+ }
745
+ }
746
+
747
+ // Apply content scan results (from pre-computed pipeline)
748
+ if (result.contentScan) {
749
+ const scan = result.contentScan;
750
+ if (scan.threatScore > 0) {
751
+ email.addHeader('X-Spam-Score', String(scan.threatScore));
752
+ if (scan.threatType) {
753
+ email.addHeader('X-Spam-Type', scan.threatType);
754
+ }
755
+ if (scan.threatScore >= 50) {
756
+ email.mightBeSpam = true;
757
+ logger.log('warn', `Content scan threat score ${scan.threatScore} (${scan.threatType}) — marking as potential spam`);
758
+ }
759
+ }
760
+ }
761
+
762
+ // Apply IP reputation results (from pre-computed pipeline)
763
+ if (result.ipReputation) {
764
+ const rep = result.ipReputation;
765
+ email.addHeader('X-IP-Reputation-Score', String(rep.score));
766
+ if (rep.is_spam) {
767
+ email.mightBeSpam = true;
768
+ logger.log('warn', `IP ${rep.ip} flagged by reputation check (score=${rep.score}) — marking as potential spam`);
769
+ }
770
+ }
771
+
772
+ 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'}`);
773
+ } catch (err) {
774
+ logger.log('warn', `Inbound security verification failed: ${(err as Error).message} — accepting email`);
775
+ }
776
+ }
777
+
778
+ /**
779
+ * Process email based on routing rules
780
+ */
781
+ public async processEmailByMode(emailData: Email | Buffer, session: IExtendedSmtpSession): Promise<Email> {
782
+ // Convert Buffer to Email if needed
783
+ let email: Email;
784
+ if (Buffer.isBuffer(emailData)) {
785
+ // Parse the email data buffer into an Email object
786
+ try {
787
+ const parsed = await plugins.mailparser.simpleParser(emailData);
788
+ email = new Email({
789
+ from: parsed.from?.value[0]?.address || session.envelope.mailFrom.address,
790
+ to: session.envelope.rcptTo[0]?.address || '',
791
+ subject: parsed.subject || '',
792
+ text: parsed.text || '',
793
+ html: parsed.html || undefined,
794
+ attachments: parsed.attachments?.map(att => ({
795
+ filename: att.filename || '',
796
+ content: att.content,
797
+ contentType: att.contentType
798
+ })) || []
799
+ });
800
+ } catch (error) {
801
+ logger.log('error', `Error parsing email data: ${error.message}`);
802
+ throw new Error(`Error parsing email data: ${error.message}`);
803
+ }
804
+ } else {
805
+ email = emailData;
806
+ }
807
+
808
+ // Run inbound security verification (DKIM/SPF/DMARC) via Rust bridge
809
+ if (session.remoteAddress && session.remoteAddress !== '127.0.0.1') {
810
+ await this.verifyInboundSecurity(email, session);
811
+ }
812
+
813
+ // First check if this is a bounce notification email
814
+ const subject = email.subject || '';
815
+ const isBounceLike = /mail delivery|delivery (failed|status|notification)|failure notice|returned mail|undeliverable|delivery problem/i.test(subject);
816
+
817
+ if (isBounceLike) {
818
+ logger.log('info', `Email subject matches bounce notification pattern: "${subject}"`);
819
+
820
+ const isBounce = await this.processBounceNotification(email);
821
+
822
+ if (isBounce) {
823
+ logger.log('info', 'Successfully processed as bounce notification, skipping regular processing');
824
+ return email;
825
+ }
826
+
827
+ logger.log('info', 'Not a valid bounce notification, continuing with regular processing');
828
+ }
829
+
830
+ // Find matching route
831
+ const context: IEmailContext = { email, session };
832
+ const route = await this.emailRouter.evaluateRoutes(context);
833
+
834
+ if (!route) {
835
+ throw new Error('No matching route for email');
836
+ }
837
+
838
+ // Store matched route in session
839
+ session.matchedRoute = route;
840
+
841
+ // Execute action based on route
842
+ await this.actionExecutor.executeAction(route.action, email, context);
843
+
844
+ // Return the processed email
845
+ return email;
846
+ }
847
+
848
+ /**
849
+ * Apply per-domain rate limits from domain configurations
850
+ */
851
+ private applyDomainRateLimits(): void {
852
+ const domainConfigs = this.domainRegistry.getAllConfigs();
853
+
854
+ for (const domainConfig of domainConfigs) {
855
+ if (domainConfig.rateLimits) {
856
+ const domain = domainConfig.domain;
857
+ const rateLimitConfig: any = {};
858
+
859
+ if (domainConfig.rateLimits.outbound) {
860
+ if (domainConfig.rateLimits.outbound.messagesPerMinute) {
861
+ rateLimitConfig.maxMessagesPerMinute = domainConfig.rateLimits.outbound.messagesPerMinute;
862
+ }
863
+ }
864
+
865
+ if (domainConfig.rateLimits.inbound) {
866
+ if (domainConfig.rateLimits.inbound.messagesPerMinute) {
867
+ rateLimitConfig.maxMessagesPerMinute = domainConfig.rateLimits.inbound.messagesPerMinute;
868
+ }
869
+ if (domainConfig.rateLimits.inbound.connectionsPerIp) {
870
+ rateLimitConfig.maxConnectionsPerIP = domainConfig.rateLimits.inbound.connectionsPerIp;
871
+ }
872
+ if (domainConfig.rateLimits.inbound.recipientsPerMessage) {
873
+ rateLimitConfig.maxRecipientsPerMessage = domainConfig.rateLimits.inbound.recipientsPerMessage;
874
+ }
875
+ }
876
+
877
+ if (Object.keys(rateLimitConfig).length > 0) {
878
+ this.rateLimiter.applyDomainLimits(domain, rateLimitConfig);
879
+ logger.log('info', `Applied rate limits for domain ${domain}:`, rateLimitConfig);
880
+ }
881
+ }
882
+ }
883
+ }
884
+
885
+ /**
886
+ * Generate SmartProxy routes for email ports
887
+ */
888
+ public generateProxyRoutes(portMapping?: Record<number, number>): any[] {
889
+ const routes: any[] = [];
890
+ const defaultPortMapping = {
891
+ 25: 10025,
892
+ 587: 10587,
893
+ 465: 10465
894
+ };
895
+
896
+ const actualPortMapping = portMapping || defaultPortMapping;
897
+
898
+ for (const externalPort of this.options.ports) {
899
+ const internalPort = actualPortMapping[externalPort] || externalPort + 10000;
900
+
901
+ let routeName = 'email-route';
902
+ let tlsMode = 'passthrough';
903
+
904
+ switch (externalPort) {
905
+ case 25:
906
+ routeName = 'smtp-route';
907
+ tlsMode = 'passthrough';
908
+ break;
909
+ case 587:
910
+ routeName = 'submission-route';
911
+ tlsMode = 'passthrough';
912
+ break;
913
+ case 465:
914
+ routeName = 'smtps-route';
915
+ tlsMode = 'terminate';
916
+ break;
917
+ default:
918
+ routeName = `email-port-${externalPort}-route`;
919
+ }
920
+
921
+ routes.push({
922
+ name: routeName,
923
+ match: {
924
+ ports: [externalPort]
925
+ },
926
+ action: {
927
+ type: 'forward',
928
+ target: {
929
+ host: 'localhost',
930
+ port: internalPort
931
+ },
932
+ tls: {
933
+ mode: tlsMode
934
+ }
935
+ }
936
+ });
937
+ }
938
+
939
+ return routes;
940
+ }
941
+
942
+ /**
943
+ * Update server configuration
944
+ */
945
+ public updateOptions(options: Partial<IUnifiedEmailServerOptions>): void {
946
+ const portsChanged = options.ports &&
947
+ (!this.options.ports ||
948
+ JSON.stringify(options.ports) !== JSON.stringify(this.options.ports));
949
+
950
+ if (portsChanged) {
951
+ this.stop().then(() => {
952
+ this.options = { ...this.options, ...options };
953
+ this.start();
954
+ });
955
+ } else {
956
+ this.options = { ...this.options, ...options };
957
+
958
+ if (options.domains) {
959
+ this.domainRegistry = new DomainRegistry(options.domains, options.defaults || this.options.defaults);
960
+ }
961
+
962
+ if (options.routes) {
963
+ this.emailRouter.updateRoutes(options.routes);
964
+ }
965
+ }
966
+ }
967
+
968
+ /**
969
+ * Update email routes
970
+ */
971
+ public updateEmailRoutes(routes: IEmailRoute[]): void {
972
+ this.options.routes = routes;
973
+ this.emailRouter.updateRoutes(routes);
974
+ }
975
+
976
+ /**
977
+ * Get server statistics
978
+ */
979
+ public getStats(): IServerStats {
980
+ return { ...this.stats };
981
+ }
982
+
983
+ /**
984
+ * Get domain registry
985
+ */
986
+ public getDomainRegistry(): DomainRegistry {
987
+ return this.domainRegistry;
988
+ }
989
+
990
+ /**
991
+ * Send an email through the delivery system
992
+ */
993
+ public async sendEmail(
994
+ email: Email,
995
+ mode: EmailProcessingMode = 'mta',
996
+ route?: IEmailRoute,
997
+ options?: {
998
+ skipSuppressionCheck?: boolean;
999
+ ipAddress?: string;
1000
+ isTransactional?: boolean;
1001
+ }
1002
+ ): Promise<string> {
1003
+ logger.log('info', `Sending email: ${email.subject} to ${email.to.join(', ')}`);
1004
+
1005
+ try {
1006
+ if (!email.from) {
1007
+ throw new Error('Email must have a sender address');
1008
+ }
1009
+
1010
+ if (!email.to || email.to.length === 0) {
1011
+ throw new Error('Email must have at least one recipient');
1012
+ }
1013
+
1014
+ // Check if any recipients are on the suppression list
1015
+ if (!options?.skipSuppressionCheck) {
1016
+ const suppressedRecipients = email.to.filter(recipient => this.isEmailSuppressed(recipient));
1017
+
1018
+ if (suppressedRecipients.length > 0) {
1019
+ const originalCount = email.to.length;
1020
+ const suppressed = suppressedRecipients.map(recipient => {
1021
+ const info = this.getSuppressionInfo(recipient);
1022
+ return {
1023
+ email: recipient,
1024
+ reason: info?.reason || 'Unknown',
1025
+ until: info?.expiresAt ? new Date(info.expiresAt).toISOString() : 'permanent'
1026
+ };
1027
+ });
1028
+
1029
+ logger.log('warn', `Filtering out ${suppressedRecipients.length} suppressed recipient(s)`, { suppressed });
1030
+
1031
+ if (suppressedRecipients.length === originalCount) {
1032
+ throw new Error('All recipients are on the suppression list');
1033
+ }
1034
+
1035
+ email.to = email.to.filter(recipient => !this.isEmailSuppressed(recipient));
1036
+ }
1037
+ }
1038
+
1039
+ // Sign with DKIM if configured
1040
+ if (mode === 'mta' && route?.action.options?.mtaOptions?.dkimSign) {
1041
+ const domain = email.from.split('@')[1];
1042
+ await this.dkimManager.handleDkimSigning(email, domain, route.action.options.mtaOptions.dkimOptions?.keySelector || 'mta');
1043
+ }
1044
+
1045
+ const id = plugins.uuid.v4();
1046
+ await this.deliveryQueue.enqueue(email, mode, route);
1047
+
1048
+ logger.log('info', `Email queued with ID: ${id}`);
1049
+ return id;
1050
+ } catch (error) {
1051
+ logger.log('error', `Failed to send email: ${error.message}`);
1052
+ throw error;
1053
+ }
1054
+ }
1055
+
1056
+ // -----------------------------------------------------------------------
1057
+ // Bounce/suppression methods
1058
+ // -----------------------------------------------------------------------
1059
+
1060
+ public async processBounceNotification(bounceEmail: Email): Promise<boolean> {
1061
+ logger.log('info', 'Processing potential bounce notification email');
1062
+
1063
+ try {
1064
+ const bounceRecord = await this.bounceManager.processBounceEmail(bounceEmail);
1065
+
1066
+ if (bounceRecord) {
1067
+ logger.log('info', `Successfully processed bounce notification for ${bounceRecord.recipient}`, {
1068
+ bounceType: bounceRecord.bounceType,
1069
+ bounceCategory: bounceRecord.bounceCategory
1070
+ });
1071
+
1072
+ this.emit('bounceProcessed', bounceRecord);
1073
+
1074
+ SecurityLogger.getInstance().logEvent({
1075
+ level: SecurityLogLevel.INFO,
1076
+ type: SecurityEventType.EMAIL_VALIDATION,
1077
+ message: `Bounce notification processed for recipient`,
1078
+ domain: bounceRecord.domain,
1079
+ details: {
1080
+ recipient: bounceRecord.recipient,
1081
+ bounceType: bounceRecord.bounceType,
1082
+ bounceCategory: bounceRecord.bounceCategory
1083
+ },
1084
+ success: true
1085
+ });
1086
+
1087
+ return true;
1088
+ } else {
1089
+ logger.log('info', 'Email not recognized as a bounce notification');
1090
+ return false;
1091
+ }
1092
+ } catch (error) {
1093
+ logger.log('error', `Error processing bounce notification: ${error.message}`);
1094
+
1095
+ SecurityLogger.getInstance().logEvent({
1096
+ level: SecurityLogLevel.ERROR,
1097
+ type: SecurityEventType.EMAIL_VALIDATION,
1098
+ message: 'Failed to process bounce notification',
1099
+ details: { error: error.message, subject: bounceEmail.subject },
1100
+ success: false
1101
+ });
1102
+
1103
+ return false;
1104
+ }
1105
+ }
1106
+
1107
+ public async processSmtpFailure(
1108
+ recipient: string,
1109
+ smtpResponse: string,
1110
+ options: { sender?: string; originalEmailId?: string; statusCode?: string; headers?: Record<string, string> } = {}
1111
+ ): Promise<boolean> {
1112
+ logger.log('info', `Processing SMTP failure for ${recipient}: ${smtpResponse}`);
1113
+
1114
+ try {
1115
+ const bounceRecord = await this.bounceManager.processSmtpFailure(recipient, smtpResponse, options);
1116
+
1117
+ logger.log('info', `Successfully processed SMTP failure for ${recipient} as ${bounceRecord.bounceCategory} bounce`, {
1118
+ bounceType: bounceRecord.bounceType
1119
+ });
1120
+
1121
+ this.emit('bounceProcessed', bounceRecord);
1122
+
1123
+ SecurityLogger.getInstance().logEvent({
1124
+ level: SecurityLogLevel.INFO,
1125
+ type: SecurityEventType.EMAIL_VALIDATION,
1126
+ message: `SMTP failure processed for recipient`,
1127
+ domain: bounceRecord.domain,
1128
+ details: {
1129
+ recipient: bounceRecord.recipient,
1130
+ bounceType: bounceRecord.bounceType,
1131
+ bounceCategory: bounceRecord.bounceCategory,
1132
+ smtpResponse
1133
+ },
1134
+ success: true
1135
+ });
1136
+
1137
+ return true;
1138
+ } catch (error) {
1139
+ logger.log('error', `Error processing SMTP failure: ${error.message}`);
1140
+
1141
+ SecurityLogger.getInstance().logEvent({
1142
+ level: SecurityLogLevel.ERROR,
1143
+ type: SecurityEventType.EMAIL_VALIDATION,
1144
+ message: 'Failed to process SMTP failure',
1145
+ details: { recipient, smtpResponse, error: error.message },
1146
+ success: false
1147
+ });
1148
+
1149
+ return false;
1150
+ }
1151
+ }
1152
+
1153
+ public isEmailSuppressed(email: string): boolean {
1154
+ return this.bounceManager.isEmailSuppressed(email);
1155
+ }
1156
+
1157
+ public getSuppressionInfo(email: string): { reason: string; timestamp: number; expiresAt?: number } | null {
1158
+ return this.bounceManager.getSuppressionInfo(email);
1159
+ }
1160
+
1161
+ public getBounceHistory(email: string): { lastBounce: number; count: number; type: BounceType; category: BounceCategory } | null {
1162
+ return this.bounceManager.getBounceInfo(email);
1163
+ }
1164
+
1165
+ public getSuppressionList(): string[] {
1166
+ return this.bounceManager.getSuppressionList();
1167
+ }
1168
+
1169
+ public getHardBouncedAddresses(): string[] {
1170
+ return this.bounceManager.getHardBouncedAddresses();
1171
+ }
1172
+
1173
+ public addToSuppressionList(email: string, reason: string, expiresAt?: number): void {
1174
+ this.bounceManager.addToSuppressionList(email, reason, expiresAt);
1175
+ logger.log('info', `Added ${email} to suppression list: ${reason}`);
1176
+ }
1177
+
1178
+ public removeFromSuppressionList(email: string): void {
1179
+ this.bounceManager.removeFromSuppressionList(email);
1180
+ logger.log('info', `Removed ${email} from suppression list`);
1181
+ }
1182
+
1183
+ public recordBounce(domain: string, receivingDomain: string, bounceType: 'hard' | 'soft', reason: string): void {
1184
+ const bounceRecord = {
1185
+ id: `bounce_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
1186
+ recipient: `user@${receivingDomain}`,
1187
+ sender: `user@${domain}`,
1188
+ domain: domain,
1189
+ bounceType: bounceType === 'hard' ? BounceType.INVALID_RECIPIENT : BounceType.TEMPORARY_FAILURE,
1190
+ bounceCategory: bounceType === 'hard' ? BounceCategory.HARD : BounceCategory.SOFT,
1191
+ timestamp: Date.now(),
1192
+ smtpResponse: reason,
1193
+ diagnosticCode: reason,
1194
+ statusCode: bounceType === 'hard' ? '550' : '450',
1195
+ processed: false
1196
+ };
1197
+
1198
+ this.bounceManager.processBounce(bounceRecord);
1199
+ }
1200
+
1201
+ /**
1202
+ * Get the rate limiter instance
1203
+ */
1204
+ public getRateLimiter(): UnifiedRateLimiter {
1205
+ return this.rateLimiter;
1206
+ }
1207
+ }