@push.rocks/smartmta 5.3.1 → 6.0.0

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 (43) hide show
  1. package/changelog.md +29 -0
  2. package/dist_rust/mailer-bin_linux_amd64 +0 -0
  3. package/dist_rust/mailer-bin_linux_arm64 +0 -0
  4. package/dist_ts/00_commitinfo_data.js +1 -1
  5. package/dist_ts/mail/core/classes.bouncemanager.d.ts +2 -1
  6. package/dist_ts/mail/core/classes.bouncemanager.js +6 -5
  7. package/dist_ts/mail/delivery/classes.delivery.queue.d.ts +3 -2
  8. package/dist_ts/mail/delivery/classes.delivery.queue.js +4 -1
  9. package/dist_ts/mail/index.d.ts +1 -0
  10. package/dist_ts/mail/index.js +2 -1
  11. package/dist_ts/mail/interfaces.storage.d.ts +7 -0
  12. package/dist_ts/mail/interfaces.storage.js +4 -0
  13. package/dist_ts/mail/routing/classes.dkim.manager.d.ts +2 -1
  14. package/dist_ts/mail/routing/classes.dkim.manager.js +17 -8
  15. package/dist_ts/mail/routing/classes.dns.manager.d.ts +1 -5
  16. package/dist_ts/mail/routing/classes.dns.manager.js +2 -2
  17. package/dist_ts/mail/routing/classes.email.router.d.ts +2 -1
  18. package/dist_ts/mail/routing/classes.email.router.js +6 -5
  19. package/dist_ts/mail/routing/classes.unified.email.server.d.ts +80 -3
  20. package/dist_ts/mail/routing/classes.unified.email.server.js +339 -47
  21. package/dist_ts/mail/routing/interfaces.d.ts +2 -0
  22. package/dist_ts/mail/security/classes.dkimcreator.d.ts +10 -5
  23. package/dist_ts/mail/security/classes.dkimcreator.js +91 -70
  24. package/dist_ts/security/classes.ipreputationchecker.d.ts +4 -3
  25. package/dist_ts/security/classes.ipreputationchecker.js +4 -3
  26. package/dist_ts/security/classes.rustsecuritybridge.d.ts +33 -1
  27. package/dist_ts/security/classes.rustsecuritybridge.js +18 -1
  28. package/package.json +19 -21
  29. package/readme.hints.md +1 -1
  30. package/readme.md +100 -30
  31. package/ts/00_commitinfo_data.ts +1 -1
  32. package/ts/mail/core/classes.bouncemanager.ts +7 -6
  33. package/ts/mail/delivery/classes.delivery.queue.ts +7 -3
  34. package/ts/mail/index.ts +2 -1
  35. package/ts/mail/interfaces.storage.ts +13 -0
  36. package/ts/mail/routing/classes.dkim.manager.ts +24 -11
  37. package/ts/mail/routing/classes.dns.manager.ts +3 -8
  38. package/ts/mail/routing/classes.email.router.ts +7 -6
  39. package/ts/mail/routing/classes.unified.email.server.ts +446 -50
  40. package/ts/mail/routing/interfaces.ts +3 -1
  41. package/ts/mail/security/classes.dkimcreator.ts +115 -91
  42. package/ts/security/classes.ipreputationchecker.ts +7 -6
  43. package/ts/security/classes.rustsecuritybridge.ts +53 -0
@@ -8,17 +8,18 @@ import {
8
8
  SecurityEventType
9
9
  } from '../../security/index.js';
10
10
  import { DKIMCreator } from '../security/classes.dkimcreator.js';
11
+ import { hasStorageManagerMethods, type IStorageManagerLike } from '../interfaces.storage.js';
11
12
  import { RustSecurityBridge } from '../../security/classes.rustsecuritybridge.js';
12
- import type { IEmailReceivedEvent, IAuthRequestEvent, IEmailData } from '../../security/classes.rustsecuritybridge.js';
13
+ import type { IEmailReceivedEvent, IRcptToRequestEvent, IAuthRequestEvent, IScramCredentialRequestEvent } from '../../security/classes.rustsecuritybridge.js';
13
14
  import { EmailRouter } from './classes.email.router.js';
14
- import type { IEmailRoute, IEmailAction, IEmailContext, IEmailDomainConfig } from './interfaces.js';
15
+ import type { IEmailRoute, IEmailContext, IEmailDomainConfig } from './interfaces.js';
15
16
  import { Email } from '../core/classes.email.js';
16
17
  import { DomainRegistry } from './classes.domain.registry.js';
17
18
  import { DnsManager } from './classes.dns.manager.js';
18
19
  import { BounceManager, BounceType, BounceCategory } from '../core/classes.bouncemanager.js';
19
20
  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';
21
+ import { MultiModeDeliverySystem, type IDeliveryStats, type IMultiModeDeliveryOptions } from '../delivery/classes.delivery.system.js';
22
+ import { UnifiedDeliveryQueue, type IQueueItem, type IQueueOptions, type IQueueStats } from '../delivery/classes.delivery.queue.js';
22
23
  import { UnifiedRateLimiter, type IHierarchicalRateLimits } from '../delivery/classes.unified.rate.limiter.js';
23
24
  import { SmtpState } from '../delivery/interfaces.js';
24
25
  import type { EmailProcessingMode, ISmtpSession as IBaseSmtpSession } from '../delivery/interfaces.js';
@@ -28,7 +29,7 @@ import { DkimManager } from './classes.dkim.manager.js';
28
29
 
29
30
  /** External DcRouter interface shape used by UnifiedEmailServer */
30
31
  interface DcRouter {
31
- storageManager: any;
32
+ storageManager: IStorageManagerLike;
32
33
  dnsServer?: any;
33
34
  options?: any;
34
35
  }
@@ -43,17 +44,51 @@ export interface IExtendedSmtpSession extends ISmtpSession {
43
44
  matchedRoute?: IEmailRoute;
44
45
  }
45
46
 
47
+ export interface ISmtpDecision {
48
+ accepted: boolean;
49
+ smtpCode?: number;
50
+ smtpMessage?: string;
51
+ }
52
+
53
+ export interface IRcptToPolicyContext {
54
+ sessionId: string;
55
+ mailFrom: string;
56
+ rcptTo: string;
57
+ acceptedRcptTo: string[];
58
+ remoteAddress: string;
59
+ clientHostname: string | null;
60
+ secure: boolean;
61
+ authenticated: boolean;
62
+ authenticatedUser: string | null;
63
+ abortSignal?: AbortSignal;
64
+ }
65
+
66
+ export interface IMessageAcceptanceContext {
67
+ email: Email;
68
+ rawMessage: Buffer;
69
+ session: IExtendedSmtpSession;
70
+ abortSignal?: AbortSignal;
71
+ }
72
+
73
+ export interface IMessageAcceptanceDecision extends ISmtpDecision {
74
+ /** Continue SmartMTA's built-in route processing after the consumer accepted responsibility. */
75
+ continueProcessing?: boolean;
76
+ }
77
+
46
78
  /**
47
79
  * Options for the unified email server
48
80
  */
49
81
  export interface IUnifiedEmailServerOptions {
50
82
  // Base server options
51
83
  ports: number[];
84
+ /** Public SMTP hostname used for greeting/banner and as the default outbound identity. */
52
85
  hostname: string;
53
86
  domains: IEmailDomainConfig[]; // Domain configurations
54
87
  banner?: string;
55
88
  debug?: boolean;
56
89
  useSocketHandler?: boolean; // Use socket-handler mode instead of port listening
90
+ /** Persist router changes back into storage when a storage manager is available. */
91
+ persistRoutes?: boolean;
57
92
 
58
93
  // Authentication options
59
94
  auth?: {
@@ -83,6 +118,27 @@ export interface IUnifiedEmailServerOptions {
83
118
  // Email routing rules
84
119
  routes: IEmailRoute[];
85
120
 
121
+ smtp?: {
122
+ /** Enable route/hook based RCPT TO validation before DATA. Defaults to true; set false only for fully trusted private listeners. */
123
+ recipientValidation?: boolean;
124
+ /** Timeout for RCPT policy callbacks. Defaults to 5000 ms. */
125
+ recipientValidationTimeoutMs?: number;
126
+ /** Timeout for message acceptance callbacks. Defaults to 30000 ms. */
127
+ messageAcceptanceTimeoutMs?: number;
128
+ /** Require trusted PROXY v1 from the backend proxy before SMTP greeting. */
129
+ proxyProtocol?: {
130
+ required?: boolean;
131
+ trustedIps?: string[];
132
+ };
133
+ };
134
+
135
+ hooks?: {
136
+ /** Async recipient policy hook, called before each RCPT TO is accepted. */
137
+ onRcptTo?: (context: IRcptToPolicyContext) => Promise<ISmtpDecision>;
138
+ /** Async message acceptance hook, called after DATA before the final SMTP response. */
139
+ onMessageData?: (context: IMessageAcceptanceContext) => Promise<IMessageAcceptanceDecision>;
140
+ };
141
+
86
142
  // Global defaults for all domains
87
143
  defaults?: {
88
144
  dnsMode?: 'forward' | 'internal-dns' | 'external-dns';
@@ -92,6 +148,8 @@ export interface IUnifiedEmailServerOptions {
92
148
 
93
149
  // Outbound settings
94
150
  outbound?: {
151
+ /** Override the SMTP identity used for outbound delivery. Defaults to `hostname`. */
152
+ hostname?: string;
95
153
  maxConnections?: number;
96
154
  connectionTimeout?: number;
97
155
  socketTimeout?: number;
@@ -99,6 +157,9 @@ export interface IUnifiedEmailServerOptions {
99
157
  defaultFrom?: string;
100
158
  };
101
159
 
160
+ // Delivery queue
161
+ queue?: IQueueOptions;
162
+
102
163
  // Rate limiting (global limits, can be overridden per domain)
103
164
  rateLimits?: IHierarchicalRateLimits;
104
165
  }
@@ -171,6 +232,11 @@ export class UnifiedEmailServer extends EventEmitter {
171
232
  // Extracted subsystems
172
233
  private actionExecutor: EmailActionExecutor;
173
234
  private dkimManager: DkimManager;
235
+ private bridgeStateChangeHandler?: (event: { oldState: string; newState: string }) => void;
236
+ private bridgeRcptToRequestHandler?: (data: IRcptToRequestEvent) => void;
237
+ private bridgeEmailReceivedHandler?: (data: IEmailReceivedEvent) => void;
238
+ private bridgeAuthRequestHandler?: (data: IAuthRequestEvent) => void;
239
+ private bridgeScramCredentialRequestHandler?: (data: IScramCredentialRequestEvent) => void;
174
240
 
175
241
  constructor(dcRouter: DcRouter, options: IUnifiedEmailServerOptions) {
176
242
  super();
@@ -206,7 +272,7 @@ export class UnifiedEmailServer extends EventEmitter {
206
272
  // Initialize email router with routes and storage manager
207
273
  this.emailRouter = new EmailRouter(options.routes || [], {
208
274
  storageManager: dcRouter.storageManager,
209
- persistChanges: true
275
+ persistChanges: options.persistRoutes ?? hasStorageManagerMethods(dcRouter.storageManager, ['get', 'set'])
210
276
  });
211
277
 
212
278
  // Initialize rate limiter
@@ -226,7 +292,8 @@ export class UnifiedEmailServer extends EventEmitter {
226
292
  storageType: 'memory', // Default to memory storage
227
293
  maxRetries: 3,
228
294
  baseRetryDelay: 300000, // 5 minutes
229
- maxRetryDelay: 3600000 // 1 hour
295
+ maxRetryDelay: 3600000, // 1 hour
296
+ ...options.queue,
230
297
  };
231
298
 
232
299
  this.deliveryQueue = new UnifiedDeliveryQueue(queueOptions);
@@ -277,6 +344,14 @@ export class UnifiedEmailServer extends EventEmitter {
277
344
  // We'll create the SMTP servers during the start() method
278
345
  }
279
346
 
347
+ private getAdvertisedHostname(): string {
348
+ return this.options.hostname;
349
+ }
350
+
351
+ private getOutboundHostname(): string {
352
+ return this.options.outbound?.hostname || this.options.hostname;
353
+ }
354
+
280
355
  /**
281
356
  * Send an outbound email via the Rust SMTP client.
282
357
  * Uses connection pooling in the Rust binary for efficiency.
@@ -314,7 +389,7 @@ export class UnifiedEmailServer extends EventEmitter {
314
389
  host,
315
390
  port,
316
391
  secure: port === 465,
317
- domain: this.options.hostname,
392
+ domain: this.getOutboundHostname(),
318
393
  auth: options?.auth,
319
394
  email: outboundEmail,
320
395
  dkim,
@@ -342,6 +417,11 @@ export class UnifiedEmailServer extends EventEmitter {
342
417
  this.emit('started');
343
418
  } catch (error) {
344
419
  logger.log('error', `Failed to start UnifiedEmailServer: ${error.message}`);
420
+ try {
421
+ await this.stop();
422
+ } catch (cleanupError) {
423
+ logger.log('warn', `Error cleaning up failed start: ${(cleanupError as Error).message}`);
424
+ }
345
425
  throw error;
346
426
  }
347
427
  }
@@ -361,11 +441,13 @@ export class UnifiedEmailServer extends EventEmitter {
361
441
  }
362
442
  logger.log('info', 'Rust security bridge started — Rust is the primary security backend');
363
443
 
364
- this.rustBridge.on('stateChange', ({ oldState, newState }: { oldState: string; newState: string }) => {
444
+ this.unregisterBridgeStateHandler();
445
+ this.bridgeStateChangeHandler = ({ oldState, newState }: { oldState: string; newState: string }) => {
365
446
  if (newState === 'failed') this.emit('bridgeFailed');
366
447
  else if (newState === 'restarting') this.emit('bridgeRestarting');
367
448
  else if (newState === 'running' && oldState === 'restarting') this.emit('bridgeRecovered');
368
- });
449
+ };
450
+ this.rustBridge.on('stateChange', this.bridgeStateChangeHandler);
369
451
  }
370
452
 
371
453
  private async initializeDkimAndDns(): Promise<void> {
@@ -384,7 +466,28 @@ export class UnifiedEmailServer extends EventEmitter {
384
466
  }
385
467
 
386
468
  private registerBridgeEventHandlers(): void {
387
- this.rustBridge.onEmailReceived(async (data) => {
469
+ this.unregisterBridgeEventHandlers();
470
+
471
+ this.bridgeRcptToRequestHandler = async (data) => {
472
+ try {
473
+ await this.handleRustRcptToRequest(data);
474
+ } catch (err) {
475
+ logger.log('error', `Error handling RCPT policy request from Rust SMTP: ${(err as Error).message}`);
476
+ try {
477
+ await this.rustBridge.sendRcptToResult({
478
+ correlationId: data.correlationId,
479
+ accepted: false,
480
+ smtpCode: 451,
481
+ smtpMessage: 'Recipient policy error',
482
+ });
483
+ } catch (sendErr) {
484
+ logger.log('warn', `Could not send RCPT policy rejection back to Rust: ${(sendErr as Error).message}`);
485
+ }
486
+ }
487
+ };
488
+ this.rustBridge.onRcptToRequest(this.bridgeRcptToRequestHandler);
489
+
490
+ this.bridgeEmailReceivedHandler = async (data) => {
388
491
  try {
389
492
  await this.handleRustEmailReceived(data);
390
493
  } catch (err) {
@@ -400,9 +503,10 @@ export class UnifiedEmailServer extends EventEmitter {
400
503
  logger.log('warn', `Could not send rejection back to Rust: ${(sendErr as Error).message}`);
401
504
  }
402
505
  }
403
- });
506
+ };
507
+ this.rustBridge.onEmailReceived(this.bridgeEmailReceivedHandler);
404
508
 
405
- this.rustBridge.onAuthRequest(async (data) => {
509
+ this.bridgeAuthRequestHandler = async (data) => {
406
510
  try {
407
511
  await this.handleRustAuthRequest(data);
408
512
  } catch (err) {
@@ -417,9 +521,10 @@ export class UnifiedEmailServer extends EventEmitter {
417
521
  logger.log('warn', `Could not send auth rejection back to Rust: ${(sendErr as Error).message}`);
418
522
  }
419
523
  }
420
- });
524
+ };
525
+ this.rustBridge.onAuthRequest(this.bridgeAuthRequestHandler);
421
526
 
422
- this.rustBridge.onScramCredentialRequest(async (data) => {
527
+ this.bridgeScramCredentialRequestHandler = async (data) => {
423
528
  try {
424
529
  await this.handleScramCredentialRequest(data);
425
530
  } catch (err) {
@@ -433,7 +538,34 @@ export class UnifiedEmailServer extends EventEmitter {
433
538
  logger.log('warn', `Could not send SCRAM credential rejection: ${(sendErr as Error).message}`);
434
539
  }
435
540
  }
436
- });
541
+ };
542
+ this.rustBridge.onScramCredentialRequest(this.bridgeScramCredentialRequestHandler);
543
+ }
544
+
545
+ private unregisterBridgeStateHandler(): void {
546
+ if (this.bridgeStateChangeHandler) {
547
+ this.rustBridge.off('stateChange', this.bridgeStateChangeHandler);
548
+ this.bridgeStateChangeHandler = undefined;
549
+ }
550
+ }
551
+
552
+ private unregisterBridgeEventHandlers(): void {
553
+ if (this.bridgeRcptToRequestHandler) {
554
+ this.rustBridge.offRcptToRequest(this.bridgeRcptToRequestHandler);
555
+ this.bridgeRcptToRequestHandler = undefined;
556
+ }
557
+ if (this.bridgeEmailReceivedHandler) {
558
+ this.rustBridge.offEmailReceived(this.bridgeEmailReceivedHandler);
559
+ this.bridgeEmailReceivedHandler = undefined;
560
+ }
561
+ if (this.bridgeAuthRequestHandler) {
562
+ this.rustBridge.offAuthRequest(this.bridgeAuthRequestHandler);
563
+ this.bridgeAuthRequestHandler = undefined;
564
+ }
565
+ if (this.bridgeScramCredentialRequestHandler) {
566
+ this.rustBridge.offScramCredentialRequest(this.bridgeScramCredentialRequestHandler);
567
+ this.bridgeScramCredentialRequestHandler = undefined;
568
+ }
437
569
  }
438
570
 
439
571
  private async startSmtpServer(): Promise<void> {
@@ -455,7 +587,7 @@ export class UnifiedEmailServer extends EventEmitter {
455
587
  const securePort = (this.options.ports as number[]).find(p => p === 465);
456
588
 
457
589
  const started = await this.rustBridge.startSmtpServer({
458
- hostname: this.options.hostname,
590
+ hostname: this.getAdvertisedHostname(),
459
591
  ports: smtpPorts,
460
592
  securePort: securePort,
461
593
  tlsCertPem,
@@ -468,7 +600,14 @@ export class UnifiedEmailServer extends EventEmitter {
468
600
  authEnabled: !!this.options.auth?.required || !!(this.options.auth?.users?.length),
469
601
  maxAuthFailures: 3,
470
602
  socketTimeoutSecs: this.options.socketTimeout ? Math.floor(this.options.socketTimeout / 1000) : 300,
471
- processingTimeoutSecs: 30,
603
+ processingTimeoutSecs: this.options.hooks?.onMessageData
604
+ ? Math.ceil(this.getMessageAcceptanceTimeoutMs() / 1000) + 1
605
+ : 30,
606
+ recipientValidationEnabled: this.isRecipientValidationEnabled(),
607
+ recipientValidationTimeoutSecs: this.options.smtp?.recipientValidationTimeoutMs
608
+ ? Math.ceil(this.options.smtp.recipientValidationTimeoutMs / 1000)
609
+ : 5,
610
+ proxyProtocol: this.options.smtp?.proxyProtocol,
472
611
  rateLimits: this.options.rateLimits ? {
473
612
  maxConnectionsPerIp: this.options.rateLimits.global?.maxConnectionsPerIP || 50,
474
613
  maxMessagesPerSender: this.options.rateLimits.global?.maxMessagesPerMinute || 100,
@@ -502,8 +641,8 @@ export class UnifiedEmailServer extends EventEmitter {
502
641
  // Clear the servers array - servers will be garbage collected
503
642
  this.servers = [];
504
643
 
505
- // Remove bridge state change listener and stop bridge
506
- this.rustBridge.removeAllListeners('stateChange');
644
+ this.unregisterBridgeEventHandlers();
645
+ this.unregisterBridgeStateHandler();
507
646
  await this.rustBridge.stop();
508
647
 
509
648
  // Stop the delivery system
@@ -518,6 +657,9 @@ export class UnifiedEmailServer extends EventEmitter {
518
657
  logger.log('info', 'Email delivery queue shut down');
519
658
  }
520
659
 
660
+ this.bounceManager.stop();
661
+ logger.log('info', 'Bounce manager stopped');
662
+
521
663
  // Close all Rust SMTP client connection pools
522
664
  try {
523
665
  await this.rustBridge.closeSmtpPool();
@@ -537,6 +679,170 @@ export class UnifiedEmailServer extends EventEmitter {
537
679
  // Rust SMTP server event handlers
538
680
  // -----------------------------------------------------------------------
539
681
 
682
+ private async handleRustRcptToRequest(data: IRcptToRequestEvent): Promise<void> {
683
+ const context: IRcptToPolicyContext = {
684
+ sessionId: data.sessionId,
685
+ mailFrom: data.mailFrom,
686
+ rcptTo: data.rcptTo,
687
+ acceptedRcptTo: data.acceptedRcptTo || [],
688
+ remoteAddress: data.remoteAddr,
689
+ clientHostname: data.clientHostname,
690
+ secure: data.secure,
691
+ authenticated: !!data.authenticatedUser,
692
+ authenticatedUser: data.authenticatedUser,
693
+ };
694
+
695
+ const hookDecision = this.options.hooks?.onRcptTo
696
+ ? await this.runHookWithTimeout(
697
+ this.getRecipientValidationTimeoutMs(),
698
+ 'Recipient policy timeout',
699
+ (abortSignal) => this.options.hooks!.onRcptTo!({ ...context, abortSignal })
700
+ )
701
+ : await this.evaluateDefaultRcptToPolicy(context);
702
+ const decision = this.options.hooks?.onRcptTo
703
+ ? await this.enforceAcceptedRcptToRelayPolicy(context, hookDecision)
704
+ : hookDecision;
705
+
706
+ await this.rustBridge.sendRcptToResult({
707
+ correlationId: data.correlationId,
708
+ accepted: decision.accepted,
709
+ smtpCode: decision.smtpCode,
710
+ smtpMessage: decision.smtpMessage,
711
+ });
712
+ }
713
+
714
+ private async evaluateDefaultRcptToPolicy(context: IRcptToPolicyContext): Promise<ISmtpDecision> {
715
+ const currentRecipientContext = { ...context, acceptedRcptTo: [] };
716
+ const email = new Email({
717
+ from: context.mailFrom || '<>',
718
+ to: [context.rcptTo],
719
+ subject: '',
720
+ text: '',
721
+ });
722
+ const session = this.buildPolicySession(currentRecipientContext);
723
+ const route = await this.emailRouter.evaluateRoutes({ email, session });
724
+
725
+ if (!route) {
726
+ return { accepted: false, smtpCode: 550, smtpMessage: 'No route for recipient' };
727
+ }
728
+ if (route.action.type === 'reject') {
729
+ return {
730
+ accepted: false,
731
+ smtpCode: route.action.reject?.code || 550,
732
+ smtpMessage: route.action.reject?.message || 'Recipient rejected',
733
+ };
734
+ }
735
+
736
+ const recipientDomain = this.getAddressDomain(context.rcptTo);
737
+ const isLocalRecipient = !!recipientDomain && this.domainRegistry.isDomainRegistered(recipientDomain);
738
+ if (!isLocalRecipient && !route.action.allowRelay) {
739
+ return { accepted: false, smtpCode: 550, smtpMessage: 'Relay not permitted' };
740
+ }
741
+
742
+ return { accepted: true, smtpCode: 250, smtpMessage: 'OK' };
743
+ }
744
+
745
+ private async enforceAcceptedRcptToRelayPolicy(
746
+ context: IRcptToPolicyContext,
747
+ decision: ISmtpDecision
748
+ ): Promise<ISmtpDecision> {
749
+ if (!decision.accepted) {
750
+ return decision;
751
+ }
752
+
753
+ const recipientDomain = this.getAddressDomain(context.rcptTo);
754
+ const isLocalRecipient = !!recipientDomain && this.domainRegistry.isDomainRegistered(recipientDomain);
755
+ if (isLocalRecipient) {
756
+ return decision;
757
+ }
758
+
759
+ const currentRecipientContext = { ...context, acceptedRcptTo: [] };
760
+ const email = new Email({
761
+ from: context.mailFrom || '<>',
762
+ to: [context.rcptTo],
763
+ subject: '',
764
+ text: '',
765
+ });
766
+ const session = this.buildPolicySession(currentRecipientContext);
767
+ const route = await this.emailRouter.evaluateRoutes({ email, session });
768
+
769
+ if (route?.action.type !== 'reject' && route?.action.allowRelay) {
770
+ return decision;
771
+ }
772
+
773
+ return { accepted: false, smtpCode: 550, smtpMessage: 'Relay not permitted' };
774
+ }
775
+
776
+ private buildPolicySession(context: IRcptToPolicyContext): IExtendedSmtpSession {
777
+ const session: IExtendedSmtpSession = {
778
+ id: context.sessionId,
779
+ state: SmtpState.RCPT_TO,
780
+ mailFrom: context.mailFrom,
781
+ rcptTo: [...context.acceptedRcptTo, context.rcptTo],
782
+ emailData: '',
783
+ useTLS: context.secure,
784
+ connectionEnded: false,
785
+ remoteAddress: context.remoteAddress,
786
+ clientHostname: context.clientHostname || '',
787
+ secure: context.secure,
788
+ authenticated: context.authenticated,
789
+ envelope: {
790
+ mailFrom: { address: context.mailFrom, args: {} },
791
+ rcptTo: [...context.acceptedRcptTo, context.rcptTo].map(address => ({ address, args: {} })),
792
+ },
793
+ };
794
+ if (context.authenticatedUser) {
795
+ session.user = { username: context.authenticatedUser };
796
+ }
797
+ return session;
798
+ }
799
+
800
+ private getAddressDomain(address: string): string | undefined {
801
+ const match = address.trim().toLowerCase().match(/@([^>\s]+)>?$/);
802
+ return match?.[1];
803
+ }
804
+
805
+ private getRecipientValidationTimeoutMs(): number {
806
+ return this.options.smtp?.recipientValidationTimeoutMs || 5000;
807
+ }
808
+
809
+ private isRecipientValidationEnabled(): boolean {
810
+ return this.options.smtp?.recipientValidation !== false;
811
+ }
812
+
813
+ private getMessageAcceptanceTimeoutMs(): number {
814
+ return this.options.smtp?.messageAcceptanceTimeoutMs || 30000;
815
+ }
816
+
817
+ private async runHookWithTimeout<T>(
818
+ timeoutMs: number,
819
+ timeoutMessage: string,
820
+ hookRunner: (abortSignal: AbortSignal) => Promise<T>,
821
+ onTimeout?: () => void
822
+ ): Promise<T> {
823
+ const abortController = new AbortController();
824
+ let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
825
+ const timeoutPromise = new Promise<never>((_resolve, reject) => {
826
+ timeoutHandle = setTimeout(() => {
827
+ abortController.abort();
828
+ onTimeout?.();
829
+ reject(new Error(timeoutMessage));
830
+ }, timeoutMs);
831
+ });
832
+
833
+ try {
834
+ return await Promise.race([
835
+ hookRunner(abortController.signal),
836
+ timeoutPromise,
837
+ ]);
838
+ } finally {
839
+ if (timeoutHandle) {
840
+ clearTimeout(timeoutHandle);
841
+ }
842
+ abortController.abort();
843
+ }
844
+ }
845
+
540
846
  /**
541
847
  * Handle an emailReceived event from the Rust SMTP server.
542
848
  */
@@ -551,12 +857,14 @@ export class UnifiedEmailServer extends EventEmitter {
551
857
  if (data.data.type === 'inline' && data.data.base64) {
552
858
  rawMessageBuffer = Buffer.from(data.data.base64, 'base64');
553
859
  } else if (data.data.type === 'file' && data.data.path) {
554
- rawMessageBuffer = plugins.fs.readFileSync(data.data.path);
555
- // Clean up temp file
556
860
  try {
557
- plugins.fs.unlinkSync(data.data.path);
558
- } catch {
559
- // Ignore cleanup errors
861
+ rawMessageBuffer = plugins.fs.readFileSync(data.data.path);
862
+ } finally {
863
+ try {
864
+ plugins.fs.unlinkSync(data.data.path);
865
+ } catch {
866
+ // Ignore cleanup errors
867
+ }
560
868
  }
561
869
  } else {
562
870
  throw new Error('Invalid email data transport');
@@ -590,15 +898,70 @@ export class UnifiedEmailServer extends EventEmitter {
590
898
  (session as any)._precomputedSecurityResults = data.securityResults;
591
899
  }
592
900
 
593
- // Process the email through the routing system
594
- await this.processEmailByMode(rawMessageBuffer, session);
901
+ const email = await this.createEmailFromBuffer(rawMessageBuffer, session);
902
+ let acceptanceDecision: IMessageAcceptanceDecision | undefined;
903
+ if (this.options.hooks?.onMessageData) {
904
+ let hookContext: IMessageAcceptanceContext | undefined;
905
+ try {
906
+ acceptanceDecision = await this.runHookWithTimeout(
907
+ this.getMessageAcceptanceTimeoutMs(),
908
+ 'Message acceptance timeout',
909
+ (abortSignal) => {
910
+ hookContext = {
911
+ email,
912
+ rawMessage: rawMessageBuffer,
913
+ session,
914
+ abortSignal,
915
+ };
916
+ return this.options.hooks!.onMessageData!(hookContext);
917
+ },
918
+ () => {
919
+ if (hookContext) {
920
+ hookContext.rawMessage = Buffer.alloc(0);
921
+ (hookContext as any).email = undefined;
922
+ (hookContext as any).session = undefined;
923
+ }
924
+ }
925
+ );
926
+ } catch (acceptanceError) {
927
+ logger.log('error', `Message acceptance hook failed: ${(acceptanceError as Error).message}`);
928
+ await this.rustBridge.sendEmailProcessingResult({
929
+ correlationId,
930
+ accepted: false,
931
+ smtpCode: 451,
932
+ smtpMessage: 'Message acceptance temporarily unavailable',
933
+ });
934
+ return;
935
+ }
936
+ }
937
+
938
+ if (acceptanceDecision && !acceptanceDecision.accepted) {
939
+ await this.rustBridge.sendEmailProcessingResult({
940
+ correlationId,
941
+ accepted: false,
942
+ smtpCode: acceptanceDecision.smtpCode || 451,
943
+ smtpMessage: acceptanceDecision.smtpMessage || 'Message not accepted',
944
+ });
945
+ return;
946
+ }
947
+
948
+ if (!acceptanceDecision || acceptanceDecision.continueProcessing) {
949
+ try {
950
+ await this.processEmailByMode(email, session);
951
+ } catch (processingError) {
952
+ if (!acceptanceDecision) {
953
+ throw processingError;
954
+ }
955
+ logger.log('error', `Post-acceptance processing failed: ${(processingError as Error).message}`);
956
+ }
957
+ }
595
958
 
596
959
  // Send acceptance back to Rust
597
960
  await this.rustBridge.sendEmailProcessingResult({
598
961
  correlationId,
599
962
  accepted: true,
600
963
  smtpCode: 250,
601
- smtpMessage: '2.0.0 Message accepted for delivery',
964
+ smtpMessage: acceptanceDecision?.smtpMessage || '2.0.0 Message accepted for delivery',
602
965
  });
603
966
  } catch (err) {
604
967
  logger.log('error', `Failed to process email from Rust SMTP: ${(err as Error).message}`);
@@ -611,6 +974,38 @@ export class UnifiedEmailServer extends EventEmitter {
611
974
  }
612
975
  }
613
976
 
977
+ private async createEmailFromBuffer(emailData: Buffer, session: IExtendedSmtpSession): Promise<Email> {
978
+ try {
979
+ const parsed = await plugins.mailparser.simpleParser(emailData);
980
+ const envelopeRecipients = session.envelope.rcptTo.map(recipient => recipient.address);
981
+ const parsedTo = ((parsed.to as any)?.value || [])
982
+ .map((recipient: any) => recipient.address)
983
+ .filter(Boolean) as string[];
984
+ const parsedCc = ((parsed.cc as any)?.value || [])
985
+ .map((recipient: any) => recipient.address)
986
+ .filter(Boolean) as string[];
987
+ return new Email({
988
+ from: parsed.from?.value[0]?.address || session.envelope.mailFrom.address,
989
+ to: envelopeRecipients.length > 0
990
+ ? envelopeRecipients
991
+ : parsedTo,
992
+ cc: parsedCc,
993
+ bcc: [],
994
+ subject: parsed.subject || '',
995
+ text: parsed.text || '',
996
+ html: parsed.html || undefined,
997
+ attachments: parsed.attachments?.map(att => ({
998
+ filename: att.filename || '',
999
+ content: att.content,
1000
+ contentType: att.contentType
1001
+ })) || []
1002
+ });
1003
+ } catch (error) {
1004
+ logger.log('error', `Error parsing email data: ${(error as Error).message}`);
1005
+ throw new Error(`Error parsing email data: ${(error as Error).message}`);
1006
+ }
1007
+ }
1008
+
614
1009
  /**
615
1010
  * Handle an authRequest event from the Rust SMTP server.
616
1011
  */
@@ -782,25 +1177,7 @@ export class UnifiedEmailServer extends EventEmitter {
782
1177
  // Convert Buffer to Email if needed
783
1178
  let email: Email;
784
1179
  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
- }
1180
+ email = await this.createEmailFromBuffer(emailData, session);
804
1181
  } else {
805
1182
  email = emailData;
806
1183
  }
@@ -973,6 +1350,10 @@ export class UnifiedEmailServer extends EventEmitter {
973
1350
  this.emailRouter.updateRoutes(routes);
974
1351
  }
975
1352
 
1353
+ public getEmailRoutes(): IEmailRoute[] {
1354
+ return this.emailRouter.getRoutes();
1355
+ }
1356
+
976
1357
  /**
977
1358
  * Get server statistics
978
1359
  */
@@ -980,6 +1361,22 @@ export class UnifiedEmailServer extends EventEmitter {
980
1361
  return { ...this.stats };
981
1362
  }
982
1363
 
1364
+ public getQueueStats(): IQueueStats {
1365
+ return this.deliveryQueue.getStats();
1366
+ }
1367
+
1368
+ public getQueueItems(): IQueueItem[] {
1369
+ return this.deliveryQueue.listItems();
1370
+ }
1371
+
1372
+ public getQueueItem(id: string): IQueueItem | undefined {
1373
+ return this.deliveryQueue.getItem(id);
1374
+ }
1375
+
1376
+ public getDeliveryStats(): IDeliveryStats {
1377
+ return this.deliverySystem.getStats();
1378
+ }
1379
+
983
1380
  /**
984
1381
  * Get domain registry
985
1382
  */
@@ -1039,11 +1436,10 @@ export class UnifiedEmailServer extends EventEmitter {
1039
1436
  // Sign with DKIM if configured
1040
1437
  if (mode === 'mta' && route?.action.options?.mtaOptions?.dkimSign) {
1041
1438
  const domain = email.from.split('@')[1];
1042
- await this.dkimManager.handleDkimSigning(email, domain, route.action.options.mtaOptions.dkimOptions?.keySelector || 'mta');
1439
+ await this.dkimManager.handleDkimSigning(email, domain, route.action.options.mtaOptions.dkimOptions?.keySelector || 'default');
1043
1440
  }
1044
1441
 
1045
- const id = plugins.uuid.v4();
1046
- await this.deliveryQueue.enqueue(email, mode, route);
1442
+ const id = await this.deliveryQueue.enqueue(email, mode, route);
1047
1443
 
1048
1444
  logger.log('info', `Email queued with ID: ${id}`);
1049
1445
  return id;