@push.rocks/smartmta 6.5.1 → 7.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 (56) hide show
  1. package/changelog.md +20 -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/index.d.ts +3 -0
  6. package/dist_ts/index.js +4 -1
  7. package/dist_ts/mail/core/classes.bouncemanager.d.ts +5 -12
  8. package/dist_ts/mail/core/classes.bouncemanager.js +27 -92
  9. package/dist_ts/mail/core/classes.email.js +2 -2
  10. package/dist_ts/mail/core/classes.templatemanager.d.ts +10 -6
  11. package/dist_ts/mail/core/classes.templatemanager.js +35 -51
  12. package/dist_ts/mail/delivery/classes.delivery.queue.d.ts +15 -20
  13. package/dist_ts/mail/delivery/classes.delivery.queue.js +196 -114
  14. package/dist_ts/mail/delivery/classes.delivery.system.js +5 -5
  15. package/dist_ts/mail/interfaces.storage.d.ts +37 -6
  16. package/dist_ts/mail/interfaces.storage.js +33 -3
  17. package/dist_ts/mail/routing/classes.dkim.manager.d.ts +2 -2
  18. package/dist_ts/mail/routing/classes.dkim.manager.js +2 -3
  19. package/dist_ts/mail/routing/classes.dns.manager.d.ts +2 -2
  20. package/dist_ts/mail/routing/classes.dns.manager.js +1 -1
  21. package/dist_ts/mail/routing/classes.email.router.d.ts +2 -2
  22. package/dist_ts/mail/routing/classes.email.router.js +4 -5
  23. package/dist_ts/mail/routing/classes.unified.email.server.d.ts +8 -6
  24. package/dist_ts/mail/routing/classes.unified.email.server.js +12 -41
  25. package/dist_ts/mail/routing/interfaces.d.ts +0 -2
  26. package/dist_ts/mail/security/classes.dkimcreator.d.ts +15 -45
  27. package/dist_ts/mail/security/classes.dkimcreator.js +66 -294
  28. package/dist_ts/paths.d.ts +0 -12
  29. package/dist_ts/paths.js +3 -36
  30. package/dist_ts/plugins.d.ts +2 -5
  31. package/dist_ts/plugins.js +3 -6
  32. package/dist_ts/security/classes.contentscanner.js +1 -2
  33. package/dist_ts/security/classes.ipreputationchecker.d.ts +6 -6
  34. package/dist_ts/security/classes.ipreputationchecker.js +24 -84
  35. package/dist_ts/security/classes.rustsecuritybridge.d.ts +3 -3
  36. package/package.json +2 -3
  37. package/readme.md +12 -6
  38. package/ts/00_commitinfo_data.ts +1 -1
  39. package/ts/index.ts +4 -0
  40. package/ts/mail/core/classes.bouncemanager.ts +31 -110
  41. package/ts/mail/core/classes.email.ts +1 -1
  42. package/ts/mail/core/classes.templatemanager.ts +42 -57
  43. package/ts/mail/delivery/classes.delivery.queue.ts +264 -135
  44. package/ts/mail/delivery/classes.delivery.system.ts +7 -5
  45. package/ts/mail/interfaces.storage.ts +64 -10
  46. package/ts/mail/routing/classes.dkim.manager.ts +3 -3
  47. package/ts/mail/routing/classes.dns.manager.ts +3 -3
  48. package/ts/mail/routing/classes.email.router.ts +6 -6
  49. package/ts/mail/routing/classes.unified.email.server.ts +23 -44
  50. package/ts/mail/routing/interfaces.ts +0 -2
  51. package/ts/mail/security/classes.dkimcreator.ts +104 -352
  52. package/ts/paths.ts +2 -41
  53. package/ts/plugins.ts +1 -6
  54. package/ts/security/classes.contentscanner.ts +0 -1
  55. package/ts/security/classes.ipreputationchecker.ts +26 -90
  56. package/ts/security/classes.rustsecuritybridge.ts +3 -3
@@ -719,11 +719,13 @@ export class MultiModeDeliverySystem extends EventEmitter {
719
719
  const keySelector = mtaOptions.dkimOptions?.keySelector || 'default';
720
720
 
721
721
  try {
722
- // Ensure DKIM keys exist for the domain
723
- await this.emailServer.dkimCreator.handleDKIMKeysForDomain(domainName);
722
+ // Ensure DKIM keys exist for the exact selector advertised in the signature.
723
+ await this.emailServer.dkimCreator.handleDKIMKeysForSelector(domainName, keySelector);
724
724
 
725
- // Get the private key
726
- const dkimPrivateKey = (await this.emailServer.dkimCreator.readDKIMKeys(domainName)).privateKey;
725
+ // Load the private key for that same selector.
726
+ const dkimPrivateKey = (
727
+ await this.emailServer.dkimCreator.readDKIMKeysForSelector(domainName, keySelector)
728
+ ).privateKey;
727
729
 
728
730
  // Convert Email to raw format for signing
729
731
  const rawEmail = email.toRFC822String();
@@ -840,4 +842,4 @@ export class MultiModeDeliverySystem extends EventEmitter {
840
842
  public getStats(): IDeliveryStats {
841
843
  return { ...this.stats };
842
844
  }
843
- }
845
+ }
@@ -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,13 @@
1
1
  import { logger } from '../../logger.js';
2
2
  import { DKIMCreator } from '../security/classes.dkimcreator.js';
3
- import { hasStorageManagerMethods, type IStorageManagerLike } from '../interfaces.storage.js';
3
+ import type { IStorageManager } from '../interfaces.storage.js';
4
4
  import { DomainRegistry } from './classes.domain.registry.js';
5
5
  import { RustSecurityBridge } from '../../security/classes.rustsecuritybridge.js';
6
6
  import { Email } from '../core/classes.email.js';
7
7
 
8
8
  /** External DcRouter interface shape used by DkimManager */
9
9
  interface DcRouter {
10
- storageManager?: IStorageManagerLike;
10
+ storageManager?: IStorageManager;
11
11
  dnsServer?: any;
12
12
  }
13
13
 
@@ -115,7 +115,7 @@ export class DkimManager {
115
115
 
116
116
  logger.log('info', `DKIM DNS handler registered for new selector: ${newSelector}._domainkey.${domain}`);
117
117
 
118
- if (hasStorageManagerMethods(this.dcRouter.storageManager, ['set'])) {
118
+ if (this.dcRouter.storageManager) {
119
119
  await this.dcRouter.storageManager.set(
120
120
  `/email/dkim/${domain}/public.key`,
121
121
  keyPair.publicKey
@@ -1,10 +1,10 @@
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
4
  import { logger } from '../../logger.js';
5
5
  /** External DcRouter interface shape used by DnsManager */
6
6
  interface IDcRouterLike {
7
- storageManager: IStorageManagerLike;
7
+ storageManager: IStorageManager;
8
8
  dnsServer?: any;
9
9
  options?: { dnsNsDomains?: string[]; dnsScopes?: string[] };
10
10
  }
@@ -50,7 +50,7 @@ interface IDnsValidationRun {
50
50
  */
51
51
  export class DnsManager {
52
52
  private dcRouter: IDcRouterLike;
53
- private storageManager: IStorageManagerLike;
53
+ private storageManager: IStorageManager;
54
54
  private resolverFactory: () => IDnsResolver;
55
55
  private activeResolvers = new Set<IDnsResolver>();
56
56
  private activeValidation?: IDnsValidationRun;
@@ -1,6 +1,6 @@
1
1
  import * as plugins from '../../plugins.js';
2
2
  import { EventEmitter } from 'node:events';
3
- import { hasStorageManagerMethods, type IStorageManagerLike } from '../interfaces.storage.js';
3
+ import type { IStorageManager } from '../interfaces.storage.js';
4
4
  import type { IEmailRoute, IEmailMatch, IEmailAction, IEmailContext } from './interfaces.js';
5
5
  import type { Email } from '../core/classes.email.js';
6
6
 
@@ -10,7 +10,7 @@ import type { Email } from '../core/classes.email.js';
10
10
  export class EmailRouter extends EventEmitter {
11
11
  private routes: IEmailRoute[];
12
12
  private patternCache: Map<string, boolean> = new Map();
13
- private storageManager?: IStorageManagerLike;
13
+ private storageManager?: IStorageManager;
14
14
  private persistChanges: boolean;
15
15
 
16
16
  /**
@@ -19,7 +19,7 @@ export class EmailRouter extends EventEmitter {
19
19
  * @param options Router options
20
20
  */
21
21
  constructor(routes: IEmailRoute[], options?: {
22
- storageManager?: IStorageManagerLike;
22
+ storageManager?: IStorageManager;
23
23
  persistChanges?: boolean;
24
24
  }) {
25
25
  super();
@@ -28,7 +28,7 @@ export class EmailRouter extends EventEmitter {
28
28
  this.persistChanges = options?.persistChanges ?? !!this.storageManager;
29
29
 
30
30
  // If storage manager is provided, try to load persisted routes
31
- if (hasStorageManagerMethods(this.storageManager, ['get'])) {
31
+ if (this.storageManager) {
32
32
  this.loadRoutes({ merge: true }).catch(error => {
33
33
  console.error(`Failed to load persisted routes: ${error.message}`);
34
34
  });
@@ -395,7 +395,7 @@ export class EmailRouter extends EventEmitter {
395
395
  * Save current routes to storage
396
396
  */
397
397
  public async saveRoutes(): Promise<void> {
398
- if (!hasStorageManagerMethods(this.storageManager, ['set'])) {
398
+ if (!this.storageManager) {
399
399
  this.emit('persistenceWarning', 'Cannot save routes: StorageManager not configured');
400
400
  return;
401
401
  }
@@ -426,7 +426,7 @@ export class EmailRouter extends EventEmitter {
426
426
  merge?: boolean; // Merge with existing routes
427
427
  replace?: boolean; // Replace existing routes
428
428
  }): Promise<IEmailRoute[]> {
429
- if (!hasStorageManagerMethods(this.storageManager, ['get'])) {
429
+ if (!this.storageManager) {
430
430
  this.emit('persistenceWarning', 'Cannot load routes: StorageManager not configured');
431
431
  return [];
432
432
  }
@@ -1,5 +1,4 @@
1
1
  import * as plugins from '../../plugins.js';
2
- import * as paths from '../../paths.js';
3
2
  import { EventEmitter } from 'events';
4
3
  import { logger } from '../../logger.js';
5
4
  import {
@@ -8,7 +7,7 @@ import {
8
7
  SecurityEventType
9
8
  } from '../../security/index.js';
10
9
  import { DKIMCreator } from '../security/classes.dkimcreator.js';
11
- import { hasStorageManagerMethods, type IStorageManagerLike } from '../interfaces.storage.js';
10
+ import type { IStorageManager } from '../interfaces.storage.js';
12
11
  import { RustSecurityBridge } from '../../security/classes.rustsecuritybridge.js';
13
12
  import type { IEmailReceivedEvent, IRcptToRequestEvent, IAuthRequestEvent, IScramCredentialRequestEvent } from '../../security/classes.rustsecuritybridge.js';
14
13
  import { EmailRouter } from './classes.email.router.js';
@@ -29,7 +28,7 @@ import { DkimManager } from './classes.dkim.manager.js';
29
28
 
30
29
  /** External DcRouter interface shape used by UnifiedEmailServer */
31
30
  interface DcRouter {
32
- storageManager: IStorageManagerLike;
31
+ storageManager: IStorageManager;
33
32
  dnsServer?: any;
34
33
  options?: any;
35
34
  }
@@ -118,9 +117,9 @@ export interface IUnifiedEmailServerOptions {
118
117
 
119
118
  // TLS options
120
119
  tls?: {
121
- certPath?: string;
122
- keyPath?: string;
123
- caPath?: string;
120
+ certPem?: string;
121
+ keyPem?: string;
122
+ caPem?: string;
124
123
  minVersion?: string;
125
124
  ciphers?: string;
126
125
  };
@@ -146,6 +145,8 @@ export interface IUnifiedEmailServerOptions {
146
145
  recipientValidationTimeoutMs?: number;
147
146
  /** Timeout for message acceptance callbacks. Defaults to 30000 ms. */
148
147
  messageAcceptanceTimeoutMs?: number;
148
+ /** Maximum complete messages concurrently held across Rust and TypeScript. */
149
+ maxConcurrentMessages?: number;
149
150
  /** Require trusted PROXY v1 from the backend proxy before SMTP greeting. */
150
151
  proxyProtocol?: {
151
152
  required?: boolean;
@@ -179,8 +180,8 @@ export interface IUnifiedEmailServerOptions {
179
180
  connectionProxyProvider?: (context: IOutboundConnectionProxyContext) => Promise<IOutboundConnectionProxy | null | undefined>;
180
181
  };
181
182
 
182
- // Delivery queue
183
- queue?: IQueueOptions;
183
+ // Queue persistence must be selected explicitly.
184
+ queue: IQueueOptions;
184
185
 
185
186
  // Rate limiting (global limits, can be overridden per domain)
186
187
  rateLimits?: IHierarchicalRateLimits;
@@ -283,7 +284,7 @@ export class UnifiedEmailServer extends EventEmitter {
283
284
  this.rustBridge = RustSecurityBridge.getInstance();
284
285
 
285
286
  // Initialize DKIM creator with storage manager
286
- this.dkimCreator = new DKIMCreator(paths.keysDir, dcRouter.storageManager);
287
+ this.dkimCreator = new DKIMCreator(dcRouter.storageManager);
287
288
 
288
289
  // Initialize bounce manager with storage manager
289
290
  this.bounceManager = new BounceManager({
@@ -298,7 +299,7 @@ export class UnifiedEmailServer extends EventEmitter {
298
299
  // Initialize email router with routes and storage manager
299
300
  this.emailRouter = new EmailRouter(options.routes || [], {
300
301
  storageManager: dcRouter.storageManager,
301
- persistChanges: options.persistRoutes ?? hasStorageManagerMethods(dcRouter.storageManager, ['get', 'set'])
302
+ persistChanges: options.persistRoutes ?? true
302
303
  });
303
304
 
304
305
  // Initialize rate limiter
@@ -315,7 +316,6 @@ export class UnifiedEmailServer extends EventEmitter {
315
316
 
316
317
  // Initialize delivery components
317
318
  const queueOptions: IQueueOptions = {
318
- storageType: 'memory', // Default to memory storage
319
319
  maxRetries: 3,
320
320
  baseRetryDelay: 300000, // 5 minutes
321
321
  maxRetryDelay: 3600000, // 1 hour
@@ -409,8 +409,12 @@ export class UnifiedEmailServer extends EventEmitter {
409
409
  let dkim: { domain: string; selector: string; privateKey: string } | undefined;
410
410
  if (options?.dkimDomain) {
411
411
  try {
412
- const { privateKey } = await this.dkimCreator.readDKIMKeys(options.dkimDomain);
413
- dkim = { domain: options.dkimDomain, selector: options.dkimSelector || 'default', privateKey };
412
+ const selector = options.dkimSelector || 'default';
413
+ const { privateKey } = await this.dkimCreator.readDKIMKeysForSelector(
414
+ options.dkimDomain,
415
+ selector,
416
+ );
417
+ dkim = { domain: options.dkimDomain, selector, privateKey };
414
418
  } catch (err) {
415
419
  logger.log('warn', `Failed to read DKIM keys for ${options.dkimDomain}: ${(err as Error).message}`);
416
420
  }
@@ -664,20 +668,11 @@ export class UnifiedEmailServer extends EventEmitter {
664
668
  }
665
669
 
666
670
  private async startSmtpServer(): Promise<void> {
667
- const hasTlsConfig = this.options.tls?.keyPath && this.options.tls?.certPath;
668
- let tlsCertPem: string | undefined;
669
- let tlsKeyPem: string | undefined;
670
-
671
- if (hasTlsConfig) {
672
- try {
673
- tlsKeyPem = plugins.fs.readFileSync(this.options.tls.keyPath!, 'utf8');
674
- tlsCertPem = plugins.fs.readFileSync(this.options.tls.certPath!, 'utf8');
675
- logger.log('info', 'TLS certificates loaded successfully');
676
- } catch (error) {
677
- logger.log('warn', `Failed to load TLS certificates: ${error.message}`);
678
- }
671
+ const tlsCertPem = this.options.tls?.certPem;
672
+ const tlsKeyPem = this.options.tls?.keyPem;
673
+ if ((tlsCertPem && !tlsKeyPem) || (!tlsCertPem && tlsKeyPem)) {
674
+ throw new Error('SMTP TLS requires both certPem and keyPem');
679
675
  }
680
-
681
676
  const configuredSecurePort = this.options.smtp?.securePort;
682
677
  if (configuredSecurePort !== undefined && !(this.options.ports as number[]).includes(configuredSecurePort)) {
683
678
  throw new Error(`Configured SMTP securePort ${configuredSecurePort} is not included in ports`);
@@ -704,6 +699,7 @@ export class UnifiedEmailServer extends EventEmitter {
704
699
  processingTimeoutSecs: this.options.hooks?.onMessageData
705
700
  ? Math.ceil(this.getMessageAcceptanceTimeoutMs() / 1000) + 1
706
701
  : 30,
702
+ maxConcurrentMessages: this.options.smtp?.maxConcurrentMessages ?? 4,
707
703
  recipientValidationEnabled: this.isRecipientValidationEnabled(),
708
704
  recipientValidationTimeoutSecs: this.options.smtp?.recipientValidationTimeoutMs
709
705
  ? Math.ceil(this.options.smtp.recipientValidationTimeoutMs / 1000)
@@ -955,24 +951,7 @@ export class UnifiedEmailServer extends EventEmitter {
955
951
  logger.log('info', `Rust SMTP received email from=${mailFrom} to=${rcptTo.join(',')} remote=${remoteAddr}`);
956
952
 
957
953
  try {
958
- // Decode the email data
959
- let rawMessageBuffer: Buffer;
960
- if (data.data.type === 'inline' && data.data.base64) {
961
- rawMessageBuffer = Buffer.from(data.data.base64, 'base64');
962
- } else if (data.data.type === 'file' && data.data.path) {
963
- try {
964
- rawMessageBuffer = plugins.fs.readFileSync(data.data.path);
965
- } finally {
966
- try {
967
- plugins.fs.unlinkSync(data.data.path);
968
- } catch {
969
- // Ignore cleanup errors
970
- }
971
- }
972
- } else {
973
- throw new Error('Invalid email data transport');
974
- }
975
-
954
+ const rawMessageBuffer = Buffer.from(data.data.base64, 'base64');
976
955
  // Build a session-like object for processEmailByMode
977
956
  const session: IExtendedSmtpSession = {
978
957
  id: data.sessionId || 'rust-' + Math.random().toString(36).substring(2),
@@ -89,7 +89,6 @@ export interface IEmailAction {
89
89
  mtaOptions?: {
90
90
  domain?: string;
91
91
  allowLocalDelivery?: boolean;
92
- localDeliveryPath?: string;
93
92
  dkimSign?: boolean;
94
93
  dkimOptions?: {
95
94
  domainName: string;
@@ -99,7 +98,6 @@ export interface IEmailAction {
99
98
  smtpBanner?: string;
100
99
  maxConnections?: number;
101
100
  connTimeout?: number;
102
- spoolDir?: string;
103
101
  };
104
102
  /** Content scanning configuration */
105
103
  contentScanning?: boolean;