@push.rocks/smartmta 6.5.2 → 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 (51) hide show
  1. package/changelog.md +10 -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 +5 -12
  6. package/dist_ts/mail/core/classes.bouncemanager.js +27 -92
  7. package/dist_ts/mail/core/classes.email.js +2 -2
  8. package/dist_ts/mail/core/classes.templatemanager.d.ts +10 -6
  9. package/dist_ts/mail/core/classes.templatemanager.js +35 -51
  10. package/dist_ts/mail/delivery/classes.delivery.queue.d.ts +15 -20
  11. package/dist_ts/mail/delivery/classes.delivery.queue.js +196 -114
  12. package/dist_ts/mail/interfaces.storage.d.ts +37 -6
  13. package/dist_ts/mail/interfaces.storage.js +33 -3
  14. package/dist_ts/mail/routing/classes.dkim.manager.d.ts +2 -2
  15. package/dist_ts/mail/routing/classes.dkim.manager.js +2 -3
  16. package/dist_ts/mail/routing/classes.dns.manager.d.ts +2 -2
  17. package/dist_ts/mail/routing/classes.dns.manager.js +1 -1
  18. package/dist_ts/mail/routing/classes.email.router.d.ts +2 -2
  19. package/dist_ts/mail/routing/classes.email.router.js +4 -5
  20. package/dist_ts/mail/routing/classes.unified.email.server.d.ts +8 -6
  21. package/dist_ts/mail/routing/classes.unified.email.server.js +9 -39
  22. package/dist_ts/mail/routing/interfaces.d.ts +0 -2
  23. package/dist_ts/mail/security/classes.dkimcreator.d.ts +15 -45
  24. package/dist_ts/mail/security/classes.dkimcreator.js +66 -294
  25. package/dist_ts/paths.d.ts +0 -12
  26. package/dist_ts/paths.js +3 -36
  27. package/dist_ts/plugins.d.ts +2 -5
  28. package/dist_ts/plugins.js +3 -6
  29. package/dist_ts/security/classes.contentscanner.js +1 -2
  30. package/dist_ts/security/classes.ipreputationchecker.d.ts +6 -6
  31. package/dist_ts/security/classes.ipreputationchecker.js +24 -84
  32. package/dist_ts/security/classes.rustsecuritybridge.d.ts +3 -3
  33. package/package.json +1 -2
  34. package/readme.md +9 -5
  35. package/ts/00_commitinfo_data.ts +1 -1
  36. package/ts/mail/core/classes.bouncemanager.ts +31 -110
  37. package/ts/mail/core/classes.email.ts +1 -1
  38. package/ts/mail/core/classes.templatemanager.ts +42 -57
  39. package/ts/mail/delivery/classes.delivery.queue.ts +264 -135
  40. package/ts/mail/interfaces.storage.ts +64 -10
  41. package/ts/mail/routing/classes.dkim.manager.ts +3 -3
  42. package/ts/mail/routing/classes.dns.manager.ts +3 -3
  43. package/ts/mail/routing/classes.email.router.ts +6 -6
  44. package/ts/mail/routing/classes.unified.email.server.ts +17 -42
  45. package/ts/mail/routing/interfaces.ts +0 -2
  46. package/ts/mail/security/classes.dkimcreator.ts +104 -352
  47. package/ts/paths.ts +2 -41
  48. package/ts/plugins.ts +1 -6
  49. package/ts/security/classes.contentscanner.ts +0 -1
  50. package/ts/security/classes.ipreputationchecker.ts +26 -90
  51. package/ts/security/classes.rustsecuritybridge.ts +3 -3
@@ -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
@@ -668,20 +668,11 @@ export class UnifiedEmailServer extends EventEmitter {
668
668
  }
669
669
 
670
670
  private async startSmtpServer(): Promise<void> {
671
- const hasTlsConfig = this.options.tls?.keyPath && this.options.tls?.certPath;
672
- let tlsCertPem: string | undefined;
673
- let tlsKeyPem: string | undefined;
674
-
675
- if (hasTlsConfig) {
676
- try {
677
- tlsKeyPem = plugins.fs.readFileSync(this.options.tls.keyPath!, 'utf8');
678
- tlsCertPem = plugins.fs.readFileSync(this.options.tls.certPath!, 'utf8');
679
- logger.log('info', 'TLS certificates loaded successfully');
680
- } catch (error) {
681
- logger.log('warn', `Failed to load TLS certificates: ${error.message}`);
682
- }
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');
683
675
  }
684
-
685
676
  const configuredSecurePort = this.options.smtp?.securePort;
686
677
  if (configuredSecurePort !== undefined && !(this.options.ports as number[]).includes(configuredSecurePort)) {
687
678
  throw new Error(`Configured SMTP securePort ${configuredSecurePort} is not included in ports`);
@@ -708,6 +699,7 @@ export class UnifiedEmailServer extends EventEmitter {
708
699
  processingTimeoutSecs: this.options.hooks?.onMessageData
709
700
  ? Math.ceil(this.getMessageAcceptanceTimeoutMs() / 1000) + 1
710
701
  : 30,
702
+ maxConcurrentMessages: this.options.smtp?.maxConcurrentMessages ?? 4,
711
703
  recipientValidationEnabled: this.isRecipientValidationEnabled(),
712
704
  recipientValidationTimeoutSecs: this.options.smtp?.recipientValidationTimeoutMs
713
705
  ? Math.ceil(this.options.smtp.recipientValidationTimeoutMs / 1000)
@@ -959,24 +951,7 @@ export class UnifiedEmailServer extends EventEmitter {
959
951
  logger.log('info', `Rust SMTP received email from=${mailFrom} to=${rcptTo.join(',')} remote=${remoteAddr}`);
960
952
 
961
953
  try {
962
- // Decode the email data
963
- let rawMessageBuffer: Buffer;
964
- if (data.data.type === 'inline' && data.data.base64) {
965
- rawMessageBuffer = Buffer.from(data.data.base64, 'base64');
966
- } else if (data.data.type === 'file' && data.data.path) {
967
- try {
968
- rawMessageBuffer = plugins.fs.readFileSync(data.data.path);
969
- } finally {
970
- try {
971
- plugins.fs.unlinkSync(data.data.path);
972
- } catch {
973
- // Ignore cleanup errors
974
- }
975
- }
976
- } else {
977
- throw new Error('Invalid email data transport');
978
- }
979
-
954
+ const rawMessageBuffer = Buffer.from(data.data.base64, 'base64');
980
955
  // Build a session-like object for processEmailByMode
981
956
  const session: IExtendedSmtpSession = {
982
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;