@push.rocks/smartmta 6.5.2 → 8.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 (84) hide show
  1. package/changelog.md +40 -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/functions.errors.d.ts +3 -0
  6. package/dist_ts/functions.errors.js +8 -0
  7. package/dist_ts/index.d.ts +3 -0
  8. package/dist_ts/index.js +4 -1
  9. package/dist_ts/mail/core/classes.bouncemanager.d.ts +16 -12
  10. package/dist_ts/mail/core/classes.bouncemanager.js +146 -129
  11. package/dist_ts/mail/core/classes.email.js +15 -13
  12. package/dist_ts/mail/core/classes.emailvalidator.d.ts +3 -3
  13. package/dist_ts/mail/core/classes.emailvalidator.js +7 -5
  14. package/dist_ts/mail/core/classes.templatemanager.d.ts +10 -6
  15. package/dist_ts/mail/core/classes.templatemanager.js +35 -51
  16. package/dist_ts/mail/delivery/classes.delivery.queue.d.ts +92 -22
  17. package/dist_ts/mail/delivery/classes.delivery.queue.js +738 -151
  18. package/dist_ts/mail/delivery/classes.delivery.system.d.ts +13 -7
  19. package/dist_ts/mail/delivery/classes.delivery.system.js +458 -145
  20. package/dist_ts/mail/delivery/classes.unified.rate.limiter.js +9 -8
  21. package/dist_ts/mail/delivery/functions.safe-observers.d.ts +10 -0
  22. package/dist_ts/mail/delivery/functions.safe-observers.js +37 -0
  23. package/dist_ts/mail/delivery/interfaces.d.ts +21 -0
  24. package/dist_ts/mail/delivery/interfaces.js +1 -1
  25. package/dist_ts/mail/interfaces.storage.d.ts +37 -6
  26. package/dist_ts/mail/interfaces.storage.js +33 -3
  27. package/dist_ts/mail/routing/classes.dkim.manager.d.ts +10 -6
  28. package/dist_ts/mail/routing/classes.dkim.manager.js +47 -31
  29. package/dist_ts/mail/routing/classes.dns.manager.d.ts +7 -5
  30. package/dist_ts/mail/routing/classes.dns.manager.js +22 -11
  31. package/dist_ts/mail/routing/classes.email.action.executor.d.ts +2 -1
  32. package/dist_ts/mail/routing/classes.email.action.executor.js +45 -16
  33. package/dist_ts/mail/routing/classes.email.router.d.ts +5 -2
  34. package/dist_ts/mail/routing/classes.email.router.js +17 -12
  35. package/dist_ts/mail/routing/classes.unified.email.server.d.ts +12 -6
  36. package/dist_ts/mail/routing/classes.unified.email.server.js +69 -78
  37. package/dist_ts/mail/routing/interfaces.d.ts +0 -2
  38. package/dist_ts/mail/security/classes.dkimcreator.d.ts +22 -45
  39. package/dist_ts/mail/security/classes.dkimcreator.js +93 -296
  40. package/dist_ts/mail/security/classes.spfverifier.js +5 -3
  41. package/dist_ts/paths.d.ts +0 -12
  42. package/dist_ts/paths.js +3 -36
  43. package/dist_ts/plugins.d.ts +2 -5
  44. package/dist_ts/plugins.js +3 -6
  45. package/dist_ts/security/classes.contentscanner.js +14 -12
  46. package/dist_ts/security/classes.ipreputationchecker.d.ts +9 -6
  47. package/dist_ts/security/classes.ipreputationchecker.js +42 -93
  48. package/dist_ts/security/classes.rustsecuritybridge.d.ts +52 -4
  49. package/dist_ts/security/classes.rustsecuritybridge.js +201 -4
  50. package/dist_ts/security/classes.securitylogger.js +7 -5
  51. package/dist_ts/security/index.d.ts +1 -1
  52. package/dist_ts/security/index.js +2 -2
  53. package/package.json +8 -9
  54. package/readme.hints.md +4 -3
  55. package/readme.md +50 -18
  56. package/readme.plan.md +6 -0
  57. package/ts/00_commitinfo_data.ts +1 -1
  58. package/ts/functions.errors.ts +8 -0
  59. package/ts/index.ts +3 -0
  60. package/ts/mail/core/classes.bouncemanager.ts +188 -155
  61. package/ts/mail/core/classes.email.ts +20 -14
  62. package/ts/mail/core/classes.emailvalidator.ts +9 -7
  63. package/ts/mail/core/classes.templatemanager.ts +42 -57
  64. package/ts/mail/delivery/classes.delivery.queue.ts +996 -185
  65. package/ts/mail/delivery/classes.delivery.system.ts +583 -170
  66. package/ts/mail/delivery/classes.unified.rate.limiter.ts +9 -8
  67. package/ts/mail/delivery/functions.safe-observers.ts +45 -0
  68. package/ts/mail/delivery/interfaces.ts +27 -1
  69. package/ts/mail/interfaces.storage.ts +64 -10
  70. package/ts/mail/routing/classes.dkim.manager.ts +65 -40
  71. package/ts/mail/routing/classes.dns.manager.ts +39 -16
  72. package/ts/mail/routing/classes.email.action.executor.ts +64 -17
  73. package/ts/mail/routing/classes.email.router.ts +20 -13
  74. package/ts/mail/routing/classes.unified.email.server.ts +107 -86
  75. package/ts/mail/routing/interfaces.ts +0 -2
  76. package/ts/mail/security/classes.dkimcreator.ts +150 -355
  77. package/ts/mail/security/classes.spfverifier.ts +4 -2
  78. package/ts/paths.ts +2 -41
  79. package/ts/plugins.ts +1 -6
  80. package/ts/security/classes.contentscanner.ts +14 -12
  81. package/ts/security/classes.ipreputationchecker.ts +46 -99
  82. package/ts/security/classes.rustsecuritybridge.ts +272 -6
  83. package/ts/security/classes.securitylogger.ts +6 -4
  84. package/ts/security/index.ts +5 -1
@@ -2,6 +2,7 @@ import * as plugins from '../../plugins.js';
2
2
  import { EventEmitter } from 'node:events';
3
3
  import { logger } from '../../logger.js';
4
4
  import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.js';
5
+ import { callObserverSafely, emitSafely } from './functions.safe-observers.js';
5
6
 
6
7
  /**
7
8
  * Interface for rate limit configuration
@@ -759,7 +760,7 @@ export class UnifiedRateLimiter extends EventEmitter {
759
760
 
760
761
  logger.log('warn', `IP ${ip} blocked due to excessive errors (${counter.errors}/${limit})`);
761
762
 
762
- SecurityLogger.getInstance().logEvent({
763
+ void callObserverSafely('rate-limit security log', () => SecurityLogger.getInstance().logEvent({
763
764
  level: SecurityLogLevel.WARN,
764
765
  type: SecurityEventType.RATE_LIMITING,
765
766
  message: 'IP blocked due to excessive errors',
@@ -769,7 +770,7 @@ export class UnifiedRateLimiter extends EventEmitter {
769
770
  limit
770
771
  },
771
772
  success: false
772
- });
773
+ }));
773
774
 
774
775
  return true;
775
776
  }
@@ -840,7 +841,7 @@ export class UnifiedRateLimiter extends EventEmitter {
840
841
 
841
842
  logger.log('warn', `IP ${ip} blocked due to excessive authentication failures (${counter.authFailures}/${limit})`);
842
843
 
843
- SecurityLogger.getInstance().logEvent({
844
+ void callObserverSafely('rate-limit security log', () => SecurityLogger.getInstance().logEvent({
844
845
  level: SecurityLogLevel.WARN,
845
846
  type: SecurityEventType.AUTHENTICATION,
846
847
  message: 'IP blocked due to excessive authentication failures',
@@ -850,7 +851,7 @@ export class UnifiedRateLimiter extends EventEmitter {
850
851
  limit
851
852
  },
852
853
  success: false
853
- });
854
+ }));
854
855
 
855
856
  return true;
856
857
  }
@@ -888,7 +889,7 @@ export class UnifiedRateLimiter extends EventEmitter {
888
889
  this.stats.currentlyBlocked++;
889
890
 
890
891
  // Emit event
891
- this.emit('ipBlocked', {
892
+ void emitSafely(this, 'ipBlocked', {
892
893
  ip,
893
894
  expiry,
894
895
  duration: duration || this.config.global.blockDuration
@@ -916,7 +917,7 @@ export class UnifiedRateLimiter extends EventEmitter {
916
917
  }
917
918
 
918
919
  // Emit event
919
- this.emit('ipUnblocked', { ip });
920
+ void emitSafely(this, 'ipUnblocked', { ip });
920
921
 
921
922
  logger.log('info', `IP ${ip} unblocked`);
922
923
  }
@@ -977,7 +978,7 @@ export class UnifiedRateLimiter extends EventEmitter {
977
978
  this.stats.activeCounters = this.counters.size + this.patternCounters.size + this.ipCounters.size;
978
979
 
979
980
  // Emit statistics update
980
- this.emit('statsUpdated', this.stats);
981
+ void emitSafely(this, 'statsUpdated', this.stats);
981
982
  }
982
983
 
983
984
  /**
@@ -1064,4 +1065,4 @@ export class UnifiedRateLimiter extends EventEmitter {
1064
1065
  public getDomainLimits(domain: string): IRateLimitConfig | undefined {
1065
1066
  return this.config.domains?.[domain];
1066
1067
  }
1067
- }
1068
+ }
@@ -0,0 +1,45 @@
1
+ import { EventEmitter } from 'node:events';
2
+
3
+ import { logger } from '../../logger.js';
4
+ import { getErrorMessage } from '../../functions.errors.js';
5
+
6
+ function logObserverFailure(message: string): void {
7
+ try {
8
+ logger.log('error', message);
9
+ } catch {
10
+ // Logging is an observer too. It must not affect the primary operation.
11
+ }
12
+ }
13
+
14
+ /**
15
+ * Notify every EventEmitter observer without allowing observer failures to
16
+ * change the operation that produced the notification. rawListeners() keeps
17
+ * EventEmitter's once wrapper semantics intact when invoked with the emitter
18
+ * as its receiver.
19
+ */
20
+ export async function emitSafely(
21
+ emitter: EventEmitter,
22
+ eventName: string | symbol,
23
+ ...args: unknown[]
24
+ ): Promise<void> {
25
+ for (const rawListener of emitter.rawListeners(eventName)) {
26
+ try {
27
+ await Reflect.apply(rawListener, emitter, args);
28
+ } catch (error) {
29
+ logObserverFailure(`Observer for ${String(eventName)} failed: ${getErrorMessage(error)}`);
30
+ }
31
+ }
32
+ }
33
+
34
+ /** Run a callback observer with the same failure isolation as emitter events. */
35
+ export async function callObserverSafely(
36
+ observerName: string,
37
+ observer: (...args: any[]) => unknown,
38
+ ...args: unknown[]
39
+ ): Promise<void> {
40
+ try {
41
+ await observer(...args);
42
+ } catch (error) {
43
+ logObserverFailure(`${observerName} observer failed: ${getErrorMessage(error)}`);
44
+ }
45
+ }
@@ -2,6 +2,33 @@
2
2
  * SMTP and email delivery interface definitions
3
3
  */
4
4
 
5
+ import type {
6
+ ISmtpRecipientResult,
7
+ ISmtpTranscriptEntry,
8
+ TSmtpDeliveryPhase,
9
+ } from '../../security/classes.rustsecuritybridge.js';
10
+
11
+ export interface ISmtpTransactionAttempt {
12
+ id: string;
13
+ queueItemId: string;
14
+ queueAttempt: number;
15
+ targetHost: string;
16
+ targetPort: number;
17
+ recipientDomain?: string;
18
+ recipients: string[];
19
+ startedAt: string;
20
+ completedAt: string;
21
+ durationMs: number;
22
+ outcome: 'succeeded' | 'failed';
23
+ retryable: boolean;
24
+ errorType?: string;
25
+ smtpCode?: number;
26
+ recipientResults?: ISmtpRecipientResult[];
27
+ failurePhase?: TSmtpDeliveryPhase;
28
+ error?: string;
29
+ transcript: ISmtpTranscriptEntry[];
30
+ }
31
+
5
32
  /**
6
33
  * SMTP session state enumeration
7
34
  */
@@ -164,4 +191,3 @@ export interface ISmtpAuth {
164
191
  */
165
192
  password: string;
166
193
  }
167
-
@@ -1,13 +1,67 @@
1
- export interface IStorageManagerLike {
2
- get?(key: string): Promise<string | null>;
3
- set?(key: string, value: string): Promise<void>;
4
- list?(prefix: string): Promise<string[]>;
5
- delete?(key: string): Promise<void>;
1
+ /**
2
+ * Durable storage for SmartMTA's small textual state.
3
+ *
4
+ * Implementations are supplied by the embedding application and are expected
5
+ * to use SmartData, SmartBucket, or another explicitly managed backend.
6
+ * SmartMTA never falls back to the local filesystem.
7
+ */
8
+ export interface IStorageManager {
9
+ get(key: string): Promise<string | null>;
10
+ set(key: string, value: string): Promise<void>;
11
+ list(prefix: string): Promise<string[]>;
12
+ delete(key: string): Promise<void>;
6
13
  }
7
14
 
8
- export function hasStorageManagerMethods<T extends keyof IStorageManagerLike>(
9
- storageManager: IStorageManagerLike | undefined,
10
- methods: T[],
11
- ): storageManager is IStorageManagerLike & Required<Pick<IStorageManagerLike, T>> {
12
- return !!storageManager && methods.every((method) => typeof storageManager[method] === 'function');
15
+ /** Blob storage for queued messages and template attachments. */
16
+ export interface IBlobStorageManager {
17
+ get(key: string): Promise<Buffer | null>;
18
+ set(key: string, value: Buffer): Promise<void>;
19
+ list(prefix: string): Promise<string[]>;
20
+ delete(key: string): Promise<void>;
13
21
  }
22
+
23
+ /** In-memory small-state store for tests and explicitly non-durable runtimes. */
24
+ export class MemoryStorageManager implements IStorageManager {
25
+ private readonly entries = new Map<string, string>();
26
+
27
+ public async get(key: string): Promise<string | null> {
28
+ return this.entries.get(key) ?? null;
29
+ }
30
+
31
+ public async set(key: string, value: string): Promise<void> {
32
+ this.entries.set(key, value);
33
+ }
34
+
35
+ public async list(prefix: string): Promise<string[]> {
36
+ return [...this.entries.keys()].filter((key) => key.startsWith(prefix)).sort();
37
+ }
38
+
39
+ public async delete(key: string): Promise<void> {
40
+ this.entries.delete(key);
41
+ }
42
+ }
43
+
44
+ /** In-memory blob store for tests and explicitly non-durable runtimes. */
45
+ export class MemoryBlobStorageManager implements IBlobStorageManager {
46
+ private readonly entries = new Map<string, Buffer>();
47
+
48
+ public async get(key: string): Promise<Buffer | null> {
49
+ const value = this.entries.get(key);
50
+ return value ? Buffer.from(value) : null;
51
+ }
52
+
53
+ public async set(key: string, value: Buffer): Promise<void> {
54
+ this.entries.set(key, Buffer.from(value));
55
+ }
56
+
57
+ public async list(prefix: string): Promise<string[]> {
58
+ return [...this.entries.keys()].filter((key) => key.startsWith(prefix)).sort();
59
+ }
60
+
61
+ public async delete(key: string): Promise<void> {
62
+ this.entries.delete(key);
63
+ }
64
+ }
65
+
66
+ /** @deprecated Use IStorageManager. */
67
+ export type IStorageManagerLike = IStorageManager;
@@ -1,13 +1,16 @@
1
1
  import { logger } from '../../logger.js';
2
+ import { getErrorMessage } from '../../functions.errors.js';
2
3
  import { DKIMCreator } from '../security/classes.dkimcreator.js';
3
- import { hasStorageManagerMethods, type IStorageManagerLike } from '../interfaces.storage.js';
4
+ import type { IStorageManager } from '../interfaces.storage.js';
4
5
  import { DomainRegistry } from './classes.domain.registry.js';
5
- import { RustSecurityBridge } from '../../security/classes.rustsecuritybridge.js';
6
6
  import { Email } from '../core/classes.email.js';
7
+ import { RustSecurityBridge } from '../../security/classes.rustsecuritybridge.js';
8
+
9
+ type TDkimKeyProvisioning = 'automatic' | 'caller-managed';
7
10
 
8
11
  /** External DcRouter interface shape used by DkimManager */
9
12
  interface DcRouter {
10
- storageManager?: IStorageManagerLike;
13
+ storageManager?: IStorageManager;
11
14
  dnsServer?: any;
12
15
  }
13
16
 
@@ -16,13 +19,30 @@ interface DcRouter {
16
19
  */
17
20
  export class DkimManager {
18
21
  private dkimKeys: Map<string, string> = new Map();
22
+ private dkimCreator: DKIMCreator;
23
+ private domainRegistry: DomainRegistry;
24
+ private dcRouter: DcRouter;
25
+ private rustBridge?: RustSecurityBridge;
26
+ private keyProvisioning: TDkimKeyProvisioning;
19
27
 
28
+ /** v7-compatible constructor shape: the fourth argument remains the Rust bridge. */
20
29
  constructor(
21
- private dkimCreator: DKIMCreator,
22
- private domainRegistry: DomainRegistry,
23
- private dcRouter: DcRouter,
24
- private rustBridge: RustSecurityBridge,
25
- ) {}
30
+ dkimCreator: DKIMCreator,
31
+ domainRegistry: DomainRegistry,
32
+ dcRouter: DcRouter,
33
+ rustBridgeOrProvisioning?: RustSecurityBridge | TDkimKeyProvisioning,
34
+ keyProvisioning: TDkimKeyProvisioning = 'automatic',
35
+ ) {
36
+ this.dkimCreator = dkimCreator;
37
+ this.domainRegistry = domainRegistry;
38
+ this.dcRouter = dcRouter;
39
+ if (typeof rustBridgeOrProvisioning === 'string') {
40
+ this.keyProvisioning = rustBridgeOrProvisioning;
41
+ } else {
42
+ this.rustBridge = rustBridgeOrProvisioning;
43
+ this.keyProvisioning = keyProvisioning;
44
+ }
45
+ }
26
46
 
27
47
  async setupDkimForDomains(): Promise<void> {
28
48
  const domainConfigs = this.domainRegistry.getAllConfigs();
@@ -36,35 +56,43 @@ export class DkimManager {
36
56
  const domain = domainConfig.domain;
37
57
  const selector = domainConfig.dkim?.selector || 'default';
38
58
 
59
+ let keyPair: { privateKey: string; publicKey: string };
39
60
  try {
40
- let keyPair: { privateKey: string; publicKey: string };
41
-
42
61
  try {
43
- keyPair = selector === 'default'
44
- ? await this.dkimCreator.readDKIMKeys(domain)
45
- : await this.dkimCreator.readDKIMKeysForSelector(domain, selector);
62
+ keyPair = await this.dkimCreator.readValidatedDKIMKeysForSelector(domain, selector);
46
63
  logger.log('info', `Using existing DKIM keys for domain: ${domain}`);
47
- } catch {
48
- await this.dkimCreator.handleDKIMKeysForSelector(
64
+ } catch (error) {
65
+ if (this.keyProvisioning === 'caller-managed') {
66
+ throw new Error(
67
+ `Caller-managed DKIM readiness failed for ${domain} (selector: ${selector}): ${(error as Error).message}`,
68
+ );
69
+ }
70
+ await this.dkimCreator.createAndStoreDKIMKeysForSelector(
49
71
  domain,
50
72
  selector,
51
73
  domainConfig.dkim?.keySize || 2048,
52
74
  );
53
- keyPair = selector === 'default'
54
- ? await this.dkimCreator.readDKIMKeys(domain)
55
- : await this.dkimCreator.readDKIMKeysForSelector(domain, selector);
75
+ keyPair = await this.dkimCreator.readValidatedDKIMKeysForSelector(domain, selector);
56
76
  logger.log('info', `Generated new DKIM keys for domain: ${domain}`);
57
77
  }
58
-
59
- this.dkimKeys.set(domain, keyPair.privateKey);
60
- logger.log('info', `DKIM keys loaded for domain: ${domain} with selector: ${selector}`);
61
78
  } catch (error) {
62
- logger.log('error', `Failed to set up DKIM for domain ${domain}: ${error.message}`);
79
+ logger.log('error', `Failed to set up DKIM for domain ${domain}: ${getErrorMessage(error)}`);
80
+ if (this.keyProvisioning === 'caller-managed') {
81
+ throw error;
82
+ }
83
+ continue;
63
84
  }
85
+
86
+ this.dkimKeys.set(domain, keyPair.privateKey);
87
+ logger.log('info', `DKIM keys loaded for domain: ${domain} with selector: ${selector}`);
64
88
  }
65
89
  }
66
90
 
67
91
  async checkAndRotateDkimKeys(): Promise<void> {
92
+ if (this.keyProvisioning === 'caller-managed') {
93
+ logger.log('debug', 'Skipping automatic DKIM rotation for caller-managed keys');
94
+ return;
95
+ }
68
96
  const domainConfigs = this.domainRegistry.getAllConfigs();
69
97
 
70
98
  for (const domainConfig of domainConfigs) {
@@ -115,7 +143,7 @@ export class DkimManager {
115
143
 
116
144
  logger.log('info', `DKIM DNS handler registered for new selector: ${newSelector}._domainkey.${domain}`);
117
145
 
118
- if (hasStorageManagerMethods(this.dcRouter.storageManager, ['set'])) {
146
+ if (this.dcRouter.storageManager) {
119
147
  await this.dcRouter.storageManager.set(
120
148
  `/email/dkim/${domain}/public.key`,
121
149
  keyPair.publicKey
@@ -131,36 +159,33 @@ export class DkimManager {
131
159
  logger.log('debug', `DKIM keys for ${domain} are up to date`);
132
160
  }
133
161
  } catch (error) {
134
- logger.log('error', `Failed to check/rotate DKIM keys for ${domain}: ${error.message}`);
162
+ logger.log('error', `Failed to check/rotate DKIM keys for ${domain}: ${getErrorMessage(error)}`);
135
163
  }
136
164
  }
137
165
  }
138
166
 
139
- async handleDkimSigning(email: Email, domain: string, selector: string): Promise<void> {
167
+ /** v7 compatibility: explicit callers may still request eager DKIM signing. */
168
+ public async handleDkimSigning(email: Email, domain: string, selector: string): Promise<void> {
140
169
  try {
141
- await this.dkimCreator.handleDKIMKeysForSelector(domain, selector);
142
- const { privateKey } = selector === 'default'
143
- ? await this.dkimCreator.readDKIMKeys(domain)
144
- : await this.dkimCreator.readDKIMKeysForSelector(domain, selector);
145
- const rawEmail = email.toRFC822String();
146
-
147
- // Detect key type from PEM header
148
- const keyType = privateKey.includes('ED25519') ? 'ed25519' : 'rsa';
149
-
150
- const signResult = await this.rustBridge.signDkim({
151
- rawMessage: rawEmail,
170
+ if (this.keyProvisioning === 'automatic') {
171
+ await this.dkimCreator.handleDKIMKeysForSelector(domain, selector);
172
+ }
173
+ const material = await this.dkimCreator.readValidatedDKIMKeysForSelector(domain, selector);
174
+ const rustBridge = this.rustBridge || RustSecurityBridge.getInstance();
175
+ const signResult = await rustBridge.signDkim({
176
+ rawMessage: email.toRFC822String(),
152
177
  domain,
153
178
  selector,
154
- privateKey,
155
- keyType,
179
+ privateKey: material.privateKey,
180
+ keyType: material.keyType,
156
181
  });
157
-
158
182
  if (signResult.header) {
159
183
  email.addHeader('DKIM-Signature', signResult.header);
160
184
  logger.log('info', `Successfully added DKIM signature for ${domain}`);
161
185
  }
162
186
  } catch (error) {
163
- logger.log('error', `Failed to sign email with DKIM: ${error.message}`);
187
+ logger.log('error', `Failed to sign email with DKIM: ${(error as Error).message}`);
188
+ if (this.keyProvisioning === 'caller-managed') throw error;
164
189
  }
165
190
  }
166
191
 
@@ -1,10 +1,12 @@
1
1
  import * as plugins from '../../plugins.js';
2
2
  import type { IEmailDomainConfig } from './interfaces.js';
3
- import type { IStorageManagerLike } from '../interfaces.storage.js';
3
+ import type { IStorageManager } from '../interfaces.storage.js';
4
+ import type { DKIMCreator } from '../security/classes.dkimcreator.js';
4
5
  import { logger } from '../../logger.js';
6
+ import { getErrorMessage } from '../../functions.errors.js';
5
7
  /** External DcRouter interface shape used by DnsManager */
6
8
  interface IDcRouterLike {
7
- storageManager: IStorageManagerLike;
9
+ storageManager: IStorageManager;
8
10
  dnsServer?: any;
9
11
  options?: { dnsNsDomains?: string[]; dnsScopes?: string[] };
10
12
  }
@@ -50,19 +52,22 @@ interface IDnsValidationRun {
50
52
  */
51
53
  export class DnsManager {
52
54
  private dcRouter: IDcRouterLike;
53
- private storageManager: IStorageManagerLike;
55
+ private storageManager: IStorageManager;
54
56
  private resolverFactory: () => IDnsResolver;
55
57
  private activeResolvers = new Set<IDnsResolver>();
58
+ private keyProvisioning: 'automatic' | 'caller-managed';
56
59
  private activeValidation?: IDnsValidationRun;
57
60
  private validationRunId = 0;
58
61
 
59
62
  constructor(
60
63
  dcRouter: IDcRouterLike,
61
- resolverFactory: () => IDnsResolver = () => new plugins.dns.promises.Resolver()
64
+ resolverFactory: () => IDnsResolver = () => new plugins.dns.promises.Resolver(),
65
+ keyProvisioning: 'automatic' | 'caller-managed' = 'automatic',
62
66
  ) {
63
67
  this.dcRouter = dcRouter;
64
68
  this.storageManager = dcRouter.storageManager;
65
69
  this.resolverFactory = resolverFactory;
70
+ this.keyProvisioning = keyProvisioning;
66
71
  }
67
72
 
68
73
  /**
@@ -125,9 +130,10 @@ export class DnsManager {
125
130
  logger.log('info', `DNS validation skipped for forward mode domain: ${config.domain}`);
126
131
  }
127
132
 
128
- // DKIM keys are still generated for consistency
129
133
  result.warnings.push(
130
- `Domain "${config.domain}" uses forward mode. DKIM keys will be generated but signing only happens if email is processed.`
134
+ this.keyProvisioning === 'automatic'
135
+ ? `Domain "${config.domain}" uses forward mode. DKIM keys will be generated but signing only happens if email is processed.`
136
+ : `Domain "${config.domain}" uses forward mode. Caller-managed DKIM keys are never generated by SmartMTA.`
131
137
  );
132
138
 
133
139
  return result;
@@ -233,7 +239,7 @@ export class DnsManager {
233
239
  return result;
234
240
  }
235
241
  result.warnings.push(
236
- `Could not verify NS delegation for ${config.domain}: ${error.message}`
242
+ `Could not verify NS delegation for ${config.domain}: ${getErrorMessage(error)}`
237
243
  );
238
244
  }
239
245
 
@@ -305,7 +311,7 @@ export class DnsManager {
305
311
  if (abortSignal?.aborted) {
306
312
  return result;
307
313
  }
308
- result.errors.push(`DNS validation failed: ${error.message}`);
314
+ result.errors.push(`DNS validation failed: ${getErrorMessage(error)}`);
309
315
  result.valid = false;
310
316
  }
311
317
 
@@ -404,7 +410,7 @@ export class DnsManager {
404
410
  return nsRecords;
405
411
  } catch (error) {
406
412
  if (!abortSignal?.aborted) {
407
- logger.log('warn', `Failed to resolve NS records for ${domain}: ${error.message}`);
413
+ logger.log('warn', `Failed to resolve NS records for ${domain}: ${getErrorMessage(error)}`);
408
414
  }
409
415
  return [];
410
416
  } finally {
@@ -449,7 +455,10 @@ export class DnsManager {
449
455
  }
450
456
 
451
457
  /** Provision deterministic local DNS records without performing network lookups. */
452
- async provisionLocalDnsRecords(domainConfigs: IEmailDomainConfig[], dkimCreator?: any): Promise<void> {
458
+ async provisionLocalDnsRecords(
459
+ domainConfigs: IEmailDomainConfig[],
460
+ dkimCreator?: DKIMCreator,
461
+ ): Promise<void> {
453
462
  logger.log('info', `Provisioning local DNS records for ${domainConfigs.length} domains`);
454
463
 
455
464
  const internalDnsDomains = domainConfigs.filter(config => config.dnsMode === 'internal-dns');
@@ -458,7 +467,7 @@ export class DnsManager {
458
467
 
459
468
  // Create DKIM records if DKIMCreator is provided
460
469
  if (dkimCreator) {
461
- await this.createDkimRecords(domainConfigs, dkimCreator);
470
+ await this.createDkimRecords(internalDnsDomains, dkimCreator);
462
471
  }
463
472
  }
464
473
  }
@@ -548,7 +557,10 @@ export class DnsManager {
548
557
  * Ensure DNS records and validate public DNS state.
549
558
  * Prefer the split lifecycle methods when validation must not block readiness.
550
559
  */
551
- async ensureDnsRecords(domainConfigs: IEmailDomainConfig[], dkimCreator?: any): Promise<void> {
560
+ async ensureDnsRecords(
561
+ domainConfigs: IEmailDomainConfig[],
562
+ dkimCreator?: DKIMCreator,
563
+ ): Promise<void> {
552
564
  await this.provisionLocalDnsRecords(domainConfigs, dkimCreator);
553
565
  await this.validateAndReportDnsRecords(domainConfigs);
554
566
  }
@@ -658,7 +670,7 @@ export class DnsManager {
658
670
  - DKIM: Will be created when keys are generated`);
659
671
 
660
672
  } catch (error) {
661
- logger.log('error', `Failed to create DNS records for ${domain}: ${error.message}`);
673
+ logger.log('error', `Failed to create DNS records for ${domain}: ${getErrorMessage(error)}`);
662
674
  }
663
675
  }
664
676
  }
@@ -666,13 +678,24 @@ export class DnsManager {
666
678
  /**
667
679
  * Create DKIM DNS records for all domains
668
680
  */
669
- private async createDkimRecords(domainConfigs: IEmailDomainConfig[], dkimCreator: any): Promise<void> {
681
+ private async createDkimRecords(
682
+ domainConfigs: IEmailDomainConfig[],
683
+ dkimCreator: DKIMCreator,
684
+ ): Promise<void> {
670
685
  for (const domainConfig of domainConfigs) {
671
686
  const domain = domainConfig.domain;
672
687
  const selector = domainConfig.dkim?.selector || 'default';
673
688
 
674
689
  try {
675
- // Get DKIM DNS record from DKIMCreator
690
+ if (this.keyProvisioning === 'caller-managed') {
691
+ // Ownership remains entirely with the embedding application. This is
692
+ // a read-only validity check: no handler, storage record, or selector
693
+ // mutation is permitted in caller-managed mode.
694
+ await dkimCreator.getDNSRecordForSelector(domain, selector);
695
+ logger.log('info', `Verified caller-managed DKIM record for ${selector}._domainkey.${domain}`);
696
+ continue;
697
+ }
698
+
676
699
  const dnsRecord = await dkimCreator.getDNSRecordForDomain(domain, selector);
677
700
 
678
701
  // For internal-dns domains, register the DNS handler
@@ -711,7 +734,7 @@ export class DnsManager {
711
734
  }
712
735
 
713
736
  } catch (error) {
714
- logger.log('warn', `Could not create DKIM DNS record for ${domain}: ${error.message}`);
737
+ logger.log('warn', `Could not create DKIM DNS record for ${domain}: ${getErrorMessage(error)}`);
715
738
  }
716
739
  }
717
740
  }