@push.rocks/smartmta 9.4.1 → 9.6.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 (40) hide show
  1. package/changelog.md +41 -0
  2. package/dist_rust/mailer-bin_linux_amd64 +0 -0
  3. package/dist_rust/mailer-bin_linux_amd64.tsrust-build.json +14 -0
  4. package/dist_rust/mailer-bin_linux_arm64 +0 -0
  5. package/dist_rust/mailer-bin_linux_arm64.tsrust-build.json +14 -0
  6. package/dist_ts/00_commitinfo_data.js +1 -1
  7. package/dist_ts/index.d.ts +2 -0
  8. package/dist_ts/index.js +3 -1
  9. package/dist_ts/mail/delivery/classes.delivery.queue.d.ts +63 -2
  10. package/dist_ts/mail/delivery/classes.delivery.queue.js +749 -193
  11. package/dist_ts/mail/delivery/classes.delivery.system.d.ts +6 -0
  12. package/dist_ts/mail/delivery/classes.delivery.system.js +146 -62
  13. package/dist_ts/mail/delivery/functions.raw-email.d.ts +9 -0
  14. package/dist_ts/mail/delivery/functions.raw-email.js +85 -0
  15. package/dist_ts/mail/delivery/symbols.delivery.queue.d.ts +6 -0
  16. package/dist_ts/mail/delivery/symbols.delivery.queue.js +7 -0
  17. package/dist_ts/mail/index.d.ts +1 -0
  18. package/dist_ts/mail/index.js +1 -1
  19. package/dist_ts/mail/routing/classes.unified.email.server.d.ts +180 -4
  20. package/dist_ts/mail/routing/classes.unified.email.server.js +592 -172
  21. package/dist_ts/mail/routing/interfaces.d.ts +41 -0
  22. package/dist_ts/mail/routing/symbols.unified.email.server.d.ts +2 -0
  23. package/dist_ts/mail/routing/symbols.unified.email.server.js +3 -0
  24. package/dist_ts/security/classes.rustsecuritybridge.d.ts +11 -0
  25. package/dist_ts/security/classes.rustsecuritybridge.js +1 -1
  26. package/package.json +6 -6
  27. package/readme.md +78 -4
  28. package/ts/00_commitinfo_data.ts +1 -1
  29. package/ts/index.ts +2 -0
  30. package/ts/mail/delivery/classes.delivery.queue.ts +923 -226
  31. package/ts/mail/delivery/classes.delivery.system.ts +192 -80
  32. package/ts/mail/delivery/functions.raw-email.ts +111 -0
  33. package/ts/mail/delivery/symbols.delivery.queue.ts +6 -0
  34. package/ts/mail/index.ts +1 -0
  35. package/ts/mail/routing/classes.unified.email.server.ts +780 -192
  36. package/ts/mail/routing/interfaces.ts +43 -0
  37. package/ts/mail/routing/symbols.unified.email.server.ts +6 -0
  38. package/ts/security/classes.rustsecuritybridge.ts +6 -0
  39. package/readme.hints.md +0 -37
  40. package/readme.plan.md +0 -30
package/changelog.md CHANGED
@@ -1,5 +1,46 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026-09-15 - 9.6.0
4
+
5
+ ### Fixes
6
+
7
+ - release construction-owned timers when the constructor rejects configuration (lifecycle)
8
+ - `UnifiedEmailServer` constructs `BounceManager` and `UnifiedRateLimiter`, whose constructors arm cleanup intervals, before later steps can still reject the configuration by throwing; a throwing constructor returns no instance, so `stop()` could never release them and the process could not exit. Construction now releases those two owners on failure and rethrows the original error.
9
+ - Clear the self-MX loop test's inject timer on every path and assert that a rejected construction leaves the active timer count unchanged.
10
+
11
+ ### Features
12
+
13
+ - add `hooks.onAuthenticate` for embedder-owned dynamic SMTP credentials (auth-hook)
14
+ - Consult the hook for `PLAIN`/`LOGIN` attempts whose username neither `auth.accounts` nor `auth.users` declares, so an application can own credentials no static list can express (per-tenant bindings, remote authorities, opaque hashes such as argon2id). Configured usernames are never handed to the hook and cannot be overridden or weakened by it.
15
+ - Enforce a hook-returned `scope` identically to a configured account scope, at both the `RCPT TO` envelope check and the `DATA` header-`From` alignment check, for the lifetime of the authenticating session.
16
+ - Bind grants to the SMTP session that authenticated so no session inherits another's scope; bound the grant table by a hard cap with oldest-first eviction plus a TTL, and refuse a session whose grant is gone with a temporary `451` and the new `onRecipientRejected` reason `auth-scope-expired` rather than treating it as unrestricted.
17
+ - Count a configured hook in `isAuthConfigured`, so a server whose credentials are entirely dynamic advertises `AUTH` with an empty account list and keeps the existing listener-restart behaviour when the hook is added or removed.
18
+ - Fail closed on a throw, a `smtp.authenticationTimeoutMs` timeout (new, default 4000 ms) or a non-object return; report SCRAM credential lookups for hook-owned usernames as not-found, since an opaque verifier cannot produce SCRAM fields.
19
+ - Match configuration's ownership of a username case-insensitively while leaving credential verification exact, so a casing variant cannot route around a configured — or deliberately disabled — account by reaching the hook instead.
20
+ - Clamp `smtp.authenticationTimeoutMs` to 4500 ms with a warning, since the SMTP listener abandons the AUTH round trip after a fixed 5000 ms and a larger value could never take effect.
21
+ - Record a rejection `message` in the server log rather than sending it to the client: every failed authentication answers with the listener's standard `535`, so the field cannot be used to probe which usernames exist.
22
+ - Refuse a missing grant independently at both the `RCPT TO` and `DATA` enforcement points, so neither depends on the other having run first.
23
+ - Add the `dynamicSmtpAuthenticationHook` capability probe, `ISmtpAuthenticateContext` and `ISmtpAuthenticateDecision` exports, readme documentation, and 21 regression tests.
24
+ - Known capacity edge: above 4096 concurrent hook-authenticated sessions, oldest-first eviction reaches live grants, and an affected client must reconnect and authenticate again rather than recovering in place. The durable fix is a listener session-close event, which the Rust connection layer does not currently emit.
25
+
26
+ ### Maintenance
27
+
28
+ - Update pnpm to 12.4.0, tstest to 6.0.0, tsrust to 1.13.0, tsbuild to 4.4.3, the gitzone CLI to 6.13.2 and Node typings to 26.5.0 for the release; deny the transitive dees-catalog build script.
29
+
30
+ ## 2026-07-31 - 9.5.0
31
+
32
+ ### Features
33
+
34
+ - add a durable raw outbound MTA queue (raw-outbound)
35
+ - Add managed-only `UnifiedEmailServer.enqueueRawEmail()` with canonical CRLF-terminated exact RFC822 bytes, a separate canonical ordered SMTP envelope, stable idempotency, a 32 MiB default message limit, and a weighted global raw-operation memory budget.
36
+ - Store raw items as atomic schema V3 queue blobs while retaining only sanitized metadata in memory; preserve structured schema V2 writes and V1/V2 hydration unchanged.
37
+ - Serialize raw status mutations and removal per item, verify payload length/digest/envelope on every lazy reread, refuse processing-item removal, and fail closed on missing or corrupt payloads.
38
+ - Isolate all public queue/status snapshots, keep queue-only operations behind internal symbols, and route internal readiness by item ID so observers cannot mutate delivery inputs.
39
+ - Reuse MX, self-MX, recipient checkpoint, retry, bounce, RemoteIngress proxy/source-evidence, timeout, TLS, pooling, capture, and cleanup behavior for raw delivery.
40
+ - Extend Rust raw SMTP delivery with mandatory caller-supplied DKIM material, canonical-byte enforcement, and actual-sent-byte capture without UTF-8 conversion; keep unsigned queue bytes immutable across attempts and capture memory reserved through verification.
41
+ - Snapshot raw caller buffers synchronously, reserve queue capacity before persistence, share the weighted memory budget with direct raw sends, cap fixed raw-operation overhead, use bounded startup hydration, close outbound admission before shutdown, drain admitted enqueue/queue-scan/delivery/authentication work before storage and Rust teardown, and recover interrupted V3 records without dropping their payload.
42
+ - Add `smartMtaCapabilities.durableRawOutboundQueue` and document the schema V3 downgrade gate and deployment-wide single-writer prerequisite.
43
+
3
44
  ## 2026-07-27 - 9.4.1
4
45
 
5
46
  ### Fixes
Binary file
@@ -0,0 +1,14 @@
1
+ {
2
+ "format": "tsrust.build-provenance.v2",
3
+ "binarySha256": "74d10e10644850c92fa7061cbabb4dc9583ef40e8c0cc2bb0024dcbdb8d80470",
4
+ "buildInfo": {
5
+ "projectName": "@push.rocks/smartmta",
6
+ "projectVersion": "9.6.0",
7
+ "gitCommit": "eac7a76f2d7e0a49006977a34ff678b638700a1f",
8
+ "gitDirty": false,
9
+ "builtAt": "2026-09-15T17:08:14.540Z",
10
+ "tsrustVersion": "1.13.0",
11
+ "binary": "mailer-bin",
12
+ "target": "linux_amd64"
13
+ }
14
+ }
Binary file
@@ -0,0 +1,14 @@
1
+ {
2
+ "format": "tsrust.build-provenance.v2",
3
+ "binarySha256": "e9372baaab19974f2ab8b353124c88330a99af54c8bc0d17a66359bee6823615",
4
+ "buildInfo": {
5
+ "projectName": "@push.rocks/smartmta",
6
+ "projectVersion": "9.6.0",
7
+ "gitCommit": "eac7a76f2d7e0a49006977a34ff678b638700a1f",
8
+ "gitDirty": false,
9
+ "builtAt": "2026-09-15T17:08:15.103Z",
10
+ "tsrustVersion": "1.13.0",
11
+ "binary": "mailer-bin",
12
+ "target": "linux_arm64"
13
+ }
14
+ }
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartmta',
6
- version: '9.4.1',
6
+ version: '9.6.0',
7
7
  description: 'A high-performance, enterprise-grade Mail Transfer Agent (MTA) built from scratch in TypeScript with Rust acceleration.'
8
8
  };
9
9
  //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSxzQkFBc0I7SUFDNUIsT0FBTyxFQUFFLE9BQU87SUFDaEIsV0FBVyxFQUFFLHlIQUF5SDtDQUN2SSxDQUFBIn0=
@@ -15,4 +15,6 @@ export declare const smartMtaCapabilities: Readonly<{
15
15
  readonly exactAcceptedEnvelopeDispatch: true;
16
16
  readonly terminalLocalDeliverActions: true;
17
17
  readonly smtpAuthAccountsAndScopes: true;
18
+ readonly durableRawOutboundQueue: true;
19
+ readonly dynamicSmtpAuthenticationHook: true;
18
20
  }>;
package/dist_ts/index.js CHANGED
@@ -15,5 +15,7 @@ export const smartMtaCapabilities = Object.freeze({
15
15
  exactAcceptedEnvelopeDispatch: true,
16
16
  terminalLocalDeliverActions: true,
17
17
  smtpAuthAccountsAndScopes: true,
18
+ durableRawOutboundQueue: true,
19
+ dynamicSmtpAuthenticationHook: true,
18
20
  });
19
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxjQUFjLHlCQUF5QixDQUFDO0FBQ3hDLGNBQWMsaUJBQWlCLENBQUM7QUFDaEMsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sRUFBRSxNQUFNLGFBQWEsQ0FBQztBQUVqRCxNQUFNLENBQUMsTUFBTSxvQkFBb0IsR0FBRyxNQUFNLENBQUMsTUFBTSxDQUFDO0lBQ2hELDBCQUEwQixFQUFFLElBQUk7SUFDaEMscUJBQXFCLEVBQUUsSUFBSTtJQUMzQiwwQkFBMEIsRUFBRSxJQUFJO0lBQ2hDLHFCQUFxQixFQUFFLElBQUk7SUFDM0Isb0JBQW9CLEVBQUUsSUFBSTtJQUMxQixpQkFBaUIsRUFBRSxJQUFJO0lBQ3ZCLG1CQUFtQixFQUFFLElBQUk7SUFDekIsbUJBQW1CLEVBQUUsSUFBSTtJQUN6Qiw0QkFBNEIsRUFBRSxJQUFJO0lBQ2xDLDZCQUE2QixFQUFFLElBQUk7SUFDbkMsMkJBQTJCLEVBQUUsSUFBSTtJQUNqQyx5QkFBeUIsRUFBRSxJQUFJO0NBQ3ZCLENBQUMsQ0FBQyJ9
21
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxjQUFjLHlCQUF5QixDQUFDO0FBQ3hDLGNBQWMsaUJBQWlCLENBQUM7QUFDaEMsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sRUFBRSxNQUFNLGFBQWEsQ0FBQztBQUVqRCxNQUFNLENBQUMsTUFBTSxvQkFBb0IsR0FBRyxNQUFNLENBQUMsTUFBTSxDQUFDO0lBQ2hELDBCQUEwQixFQUFFLElBQUk7SUFDaEMscUJBQXFCLEVBQUUsSUFBSTtJQUMzQiwwQkFBMEIsRUFBRSxJQUFJO0lBQ2hDLHFCQUFxQixFQUFFLElBQUk7SUFDM0Isb0JBQW9CLEVBQUUsSUFBSTtJQUMxQixpQkFBaUIsRUFBRSxJQUFJO0lBQ3ZCLG1CQUFtQixFQUFFLElBQUk7SUFDekIsbUJBQW1CLEVBQUUsSUFBSTtJQUN6Qiw0QkFBNEIsRUFBRSxJQUFJO0lBQ2xDLDZCQUE2QixFQUFFLElBQUk7SUFDbkMsMkJBQTJCLEVBQUUsSUFBSTtJQUNqQyx5QkFBeUIsRUFBRSxJQUFJO0lBQy9CLHVCQUF1QixFQUFFLElBQUk7SUFDN0IsNkJBQTZCLEVBQUUsSUFBSTtDQUMzQixDQUFDLENBQUMifQ==
@@ -1,9 +1,11 @@
1
1
  import { EventEmitter } from 'node:events';
2
- import type { IBlobStorageManager } from '../interfaces.storage.js';
2
+ import { type IBlobStorageManager } from '../interfaces.storage.js';
3
3
  import { type EmailProcessingMode } from './interfaces.js';
4
4
  import type { ISmtpTransactionAttempt } from './interfaces.js';
5
5
  import type { IEmailRoute } from '../routing/interfaces.js';
6
6
  import { Email } from '../core/classes.email.js';
7
+ import { type IRawSmtpEnvelope } from './functions.raw-email.js';
8
+ import { kQueueCloseAdmission, kQueueCreateItemSnapshot, kQueueEnqueueRaw, kQueueReserveRawAccess, kQueueSetReadyItemsHandler, kQueueWithRawMessage } from './symbols.delivery.queue.js';
7
9
  /**
8
10
  * Queue item status
9
11
  */
@@ -47,6 +49,14 @@ export interface IQueueItem {
47
49
  idempotencyFingerprint?: string;
48
50
  processingMode: EmailProcessingMode;
49
51
  processingResult: Email;
52
+ /** Present only for durable exact-byte outbound queue items. */
53
+ messageFormat?: 'raw-rfc822';
54
+ /** SHA-256 of the immutable unsigned RFC822 bytes. */
55
+ rawMessageSha256?: string;
56
+ /** Exact immutable unsigned RFC822 byte length. */
57
+ rawMessageLength?: number;
58
+ /** Canonical SMTP envelope, independent from message headers. */
59
+ smtpEnvelope?: IRawSmtpEnvelope;
50
60
  /** Materialized attachment count for queue/detail consumers. */
51
61
  attachmentCount?: number;
52
62
  route?: IEmailRoute;
@@ -64,6 +74,8 @@ export interface IQueueItem {
64
74
  smtpTransactions?: ISmtpTransactionAttempt[];
65
75
  }
66
76
  export declare const ACCEPTED_ENVELOPE_IDEMPOTENCY_RETENTION_MS: number;
77
+ export declare const DEFAULT_MAX_RAW_MESSAGE_BYTES: number;
78
+ export declare const DEFAULT_RAW_IN_FLIGHT_BYTE_BUDGET: number;
67
79
  export interface IQueueFailureOptions {
68
80
  /** Unknown failures default to retryable so temporary internal faults do not lose mail. */
69
81
  retryable?: boolean;
@@ -91,6 +103,10 @@ export interface IQueueOptions {
91
103
  checkInterval?: number;
92
104
  maxQueueSize?: number;
93
105
  maxPerDestination?: number;
106
+ /** Maximum exact RFC822 bytes accepted by enqueueRawEmail. Defaults to 32 MiB. */
107
+ maxRawMessageBytes?: number;
108
+ /** Conservative memory budget for active raw operations. Defaults to 192 MiB. */
109
+ rawInFlightByteBudget?: number;
94
110
  maxRetries?: number;
95
111
  baseRetryDelay?: number;
96
112
  maxRetryDelay?: number;
@@ -103,6 +119,11 @@ export interface IQueueEnqueueOptions {
103
119
  /** Fail closed unless the queue is backed by managed blob storage. */
104
120
  requireDurable?: boolean;
105
121
  }
122
+ interface IRawQueueEnqueueOptions {
123
+ rawMessage: Buffer;
124
+ envelope: IRawSmtpEnvelope;
125
+ idempotencyKey: string;
126
+ }
106
127
  export interface IQueueStats {
107
128
  queueSize: number;
108
129
  status: {
@@ -131,26 +152,46 @@ export declare class UnifiedDeliveryQueue extends EventEmitter {
131
152
  private queue;
132
153
  private checkTimer?;
133
154
  private cleanupTimer?;
155
+ private cleanupFlight?;
134
156
  private stats;
135
157
  private processing;
136
158
  private processingScanPromise?;
137
159
  private processingRescanRequested;
138
- private processingScanShutdownTimeoutMs;
139
160
  private totalProcessed;
140
161
  private ownsManagedPrefix;
141
162
  private lifecycleState;
142
163
  private readonly idempotentEnqueueFlights;
143
164
  private idempotencyTombstoneCleanupCursor?;
165
+ private readonly itemMutationFlights;
166
+ private rawBytesInFlight;
167
+ private rawByteWaiters;
168
+ private rawReservationCount;
169
+ private rawReservationDrainWaiters;
170
+ private readyItemsHandler?;
171
+ private acceptingNewItems;
172
+ private acceptingMutations;
173
+ private acceptingRawReservations;
174
+ private readonly enqueueFlights;
175
+ private pendingNewItemCount;
176
+ private shutdownFlight?;
144
177
  /**
145
178
  * Create a new unified delivery queue
146
179
  * @param options Queue options
147
180
  */
148
181
  constructor(options: IQueueOptions);
149
182
  private assertMutable;
183
+ private assertAcceptingNewItems;
184
+ private trackEnqueueFlight;
150
185
  /** Whether queue entries are committed through caller-managed blob storage. */
151
186
  hasManagedStorage(): boolean;
152
187
  /** Whether managed storage can list a backend-bounded page of keys. */
153
188
  hasPaginatedManagedStorage(): boolean;
189
+ [kQueueCloseAdmission](): void;
190
+ [kQueueSetReadyItemsHandler](handlerArg?: (itemIds: string[]) => Promise<void>): void;
191
+ private cloneEmail;
192
+ [kQueueCreateItemSnapshot](itemArg: IQueueItem): IQueueItem;
193
+ private emitItemEventSafely;
194
+ private emitReadyItemsSafely;
154
195
  private idempotentQueueId;
155
196
  private idempotencyTombstonePrefix;
156
197
  private idempotencyTombstoneKey;
@@ -172,6 +213,7 @@ export declare class UnifiedDeliveryQueue extends EventEmitter {
172
213
  * Start queue processing
173
214
  */
174
215
  private startProcessing;
216
+ private runPeriodicCleanup;
175
217
  /**
176
218
  * Stop queue processing
177
219
  */
@@ -193,6 +235,10 @@ export declare class UnifiedDeliveryQueue extends EventEmitter {
193
235
  * @param route Email route
194
236
  */
195
237
  enqueue(processingResult: Email, mode: EmailProcessingMode, route?: IEmailRoute, enqueueOptions?: IQueueEnqueueOptions): Promise<string>;
238
+ private enqueueAccepted;
239
+ [kQueueEnqueueRaw](optionsArg: IRawQueueEnqueueOptions): Promise<string>;
240
+ private enqueueRawAccepted;
241
+ private enqueueIdempotent;
196
242
  private enqueueNew;
197
243
  /**
198
244
  * Get an item from the queue
@@ -253,8 +299,21 @@ export declare class UnifiedDeliveryQueue extends EventEmitter {
253
299
  removeItem(id: string): Promise<boolean>;
254
300
  private storageKey;
255
301
  private persistItem;
302
+ private persistCurrentItem;
256
303
  private serializeItem;
304
+ private withItemMutex;
257
305
  private applyPersistedMutation;
306
+ private createRawIntegrityError;
307
+ private readVerifiedRawMessage;
308
+ private claimRawReservation;
309
+ private createRawByteRelease;
310
+ private reserveRawBytes;
311
+ private reserveRawBytesImmediately;
312
+ [kQueueReserveRawAccess](rawMessageLengthArg: number): () => void;
313
+ private releaseRawBytes;
314
+ private withRawByteReservation;
315
+ private waitForRawReservationsToDrain;
316
+ [kQueueWithRawMessage]<T>(itemIdArg: string, callbackArg: (rawMessage: Buffer) => Promise<T>): Promise<T>;
258
317
  private removeItemFromStorage;
259
318
  private deserializeItem;
260
319
  private loadFromStorage;
@@ -283,4 +342,6 @@ export declare class UnifiedDeliveryQueue extends EventEmitter {
283
342
  * Shutdown the queue
284
343
  */
285
344
  shutdown(): Promise<void>;
345
+ private shutdownOnce;
286
346
  }
347
+ export {};