@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
@@ -0,0 +1,9 @@
1
+ export interface IRawSmtpEnvelope {
2
+ from: string;
3
+ to: string[];
4
+ }
5
+ export declare const MAX_RAW_SMTP_RECIPIENTS = 100;
6
+ export declare const assertCanonicalRawRfc822Message: (rawMessageArg: Buffer) => void;
7
+ export declare const normalizeRawSmtpEnvelope: (envelopeArg: IRawSmtpEnvelope) => IRawSmtpEnvelope;
8
+ export declare const cloneRawSmtpEnvelope: (envelopeArg: IRawSmtpEnvelope) => IRawSmtpEnvelope;
9
+ export declare const createRawQueueFingerprint: (rawMessageArg: Buffer, envelopeArg: IRawSmtpEnvelope) => string;
@@ -0,0 +1,85 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { domainToASCII } from 'node:url';
3
+ import { Email } from '../core/classes.email.js';
4
+ export const MAX_RAW_SMTP_RECIPIENTS = 100;
5
+ export const assertCanonicalRawRfc822Message = (rawMessageArg) => {
6
+ if (!Buffer.isBuffer(rawMessageArg) || rawMessageArg.length === 0) {
7
+ throw new Error('Raw outbound message must be a nonempty Buffer');
8
+ }
9
+ for (let index = 0; index < rawMessageArg.length; index++) {
10
+ const byte = rawMessageArg[index];
11
+ if (byte === 0x0d && rawMessageArg[index + 1] !== 0x0a) {
12
+ throw new Error('Raw outbound message must use canonical CRLF line endings');
13
+ }
14
+ if (byte === 0x0a && (index === 0 || rawMessageArg[index - 1] !== 0x0d)) {
15
+ throw new Error('Raw outbound message must use canonical CRLF line endings');
16
+ }
17
+ }
18
+ if (rawMessageArg.length < 2
19
+ || rawMessageArg[rawMessageArg.length - 2] !== 0x0d
20
+ || rawMessageArg[rawMessageArg.length - 1] !== 0x0a) {
21
+ throw new Error('Raw outbound message must end with CRLF');
22
+ }
23
+ };
24
+ const writeLengthFramed = (hashArg, labelArg, valueArg) => {
25
+ const label = Buffer.from(labelArg, 'utf8');
26
+ const labelLength = Buffer.allocUnsafe(4);
27
+ labelLength.writeUInt32BE(label.length);
28
+ const valueLength = Buffer.allocUnsafe(8);
29
+ valueLength.writeBigUInt64BE(BigInt(valueArg.length));
30
+ hashArg.update(labelLength).update(label).update(valueLength).update(valueArg);
31
+ };
32
+ const normalizeRawMailbox = (valueArg, fieldArg) => {
33
+ if (typeof valueArg !== 'string' || /[\r\n<>]/.test(valueArg)) {
34
+ throw new Error(`Raw SMTP envelope ${fieldArg} must be a plain mailbox address`);
35
+ }
36
+ const value = valueArg.trim();
37
+ const atIndex = value.indexOf('@');
38
+ if (atIndex < 1 || atIndex !== value.lastIndexOf('@') || atIndex === value.length - 1) {
39
+ throw new Error(`Raw SMTP envelope ${fieldArg} must be a plain mailbox address`);
40
+ }
41
+ const asciiDomain = domainToASCII(value.slice(atIndex + 1));
42
+ if (!asciiDomain) {
43
+ throw new Error(`Raw SMTP envelope ${fieldArg} has an invalid domain`);
44
+ }
45
+ const normalized = `${value.slice(0, atIndex).toLowerCase()}@${asciiDomain.toLowerCase()}`;
46
+ try {
47
+ new Email({ from: normalized, to: normalized, subject: '', text: '' });
48
+ }
49
+ catch {
50
+ throw new Error(`Raw SMTP envelope ${fieldArg} must be a valid mailbox address`);
51
+ }
52
+ return normalized;
53
+ };
54
+ export const normalizeRawSmtpEnvelope = (envelopeArg) => {
55
+ if (!envelopeArg || typeof envelopeArg !== 'object') {
56
+ throw new Error('Raw SMTP envelope is required');
57
+ }
58
+ if (!Array.isArray(envelopeArg.to)
59
+ || envelopeArg.to.length === 0
60
+ || envelopeArg.to.length > MAX_RAW_SMTP_RECIPIENTS) {
61
+ throw new Error(`Raw SMTP envelope requires 1 to ${MAX_RAW_SMTP_RECIPIENTS} recipients`);
62
+ }
63
+ const from = normalizeRawMailbox(envelopeArg.from, 'sender');
64
+ const to = envelopeArg.to.map((recipient, index) => (normalizeRawMailbox(recipient, `recipient ${index + 1}`)));
65
+ if (new Set(to).size !== to.length) {
66
+ throw new Error('Raw SMTP envelope recipients must be unique after canonicalization');
67
+ }
68
+ return { from, to };
69
+ };
70
+ export const cloneRawSmtpEnvelope = (envelopeArg) => ({
71
+ from: envelopeArg.from,
72
+ to: [...envelopeArg.to],
73
+ });
74
+ export const createRawQueueFingerprint = (rawMessageArg, envelopeArg) => {
75
+ const hash = createHash('sha256');
76
+ hash.update('smartmta:durable-raw-outbound-queue:fingerprint:v1\0');
77
+ writeLengthFramed(hash, 'raw-rfc822-bytes', rawMessageArg);
78
+ writeLengthFramed(hash, 'smtp-envelope-from', Buffer.from(envelopeArg.from, 'utf8'));
79
+ for (const recipient of envelopeArg.to) {
80
+ writeLengthFramed(hash, 'smtp-envelope-recipient', Buffer.from(recipient, 'utf8'));
81
+ }
82
+ writeLengthFramed(hash, 'processing-semantics', Buffer.from('raw-mta;dkim=required-current-sender-domain-key;transform=none', 'utf8'));
83
+ return hash.digest('hex');
84
+ };
85
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZnVuY3Rpb25zLnJhdy1lbWFpbC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3RzL21haWwvZGVsaXZlcnkvZnVuY3Rpb25zLnJhdy1lbWFpbC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsVUFBVSxFQUFhLE1BQU0sYUFBYSxDQUFDO0FBQ3BELE9BQU8sRUFBRSxhQUFhLEVBQUUsTUFBTSxVQUFVLENBQUM7QUFFekMsT0FBTyxFQUFFLEtBQUssRUFBRSxNQUFNLDBCQUEwQixDQUFDO0FBT2pELE1BQU0sQ0FBQyxNQUFNLHVCQUF1QixHQUFHLEdBQUcsQ0FBQztBQUUzQyxNQUFNLENBQUMsTUFBTSwrQkFBK0IsR0FBRyxDQUFDLGFBQXFCLEVBQVEsRUFBRTtJQUM3RSxJQUFJLENBQUMsTUFBTSxDQUFDLFFBQVEsQ0FBQyxhQUFhLENBQUMsSUFBSSxhQUFhLENBQUMsTUFBTSxLQUFLLENBQUMsRUFBRSxDQUFDO1FBQ2xFLE1BQU0sSUFBSSxLQUFLLENBQUMsZ0RBQWdELENBQUMsQ0FBQztJQUNwRSxDQUFDO0lBQ0QsS0FBSyxJQUFJLEtBQUssR0FBRyxDQUFDLEVBQUUsS0FBSyxHQUFHLGFBQWEsQ0FBQyxNQUFNLEVBQUUsS0FBSyxFQUFFLEVBQUUsQ0FBQztRQUMxRCxNQUFNLElBQUksR0FBRyxhQUFhLENBQUMsS0FBSyxDQUFDLENBQUM7UUFDbEMsSUFBSSxJQUFJLEtBQUssSUFBSSxJQUFJLGFBQWEsQ0FBQyxLQUFLLEdBQUcsQ0FBQyxDQUFDLEtBQUssSUFBSSxFQUFFLENBQUM7WUFDdkQsTUFBTSxJQUFJLEtBQUssQ0FBQywyREFBMkQsQ0FBQyxDQUFDO1FBQy9FLENBQUM7UUFDRCxJQUFJLElBQUksS0FBSyxJQUFJLElBQUksQ0FBQyxLQUFLLEtBQUssQ0FBQyxJQUFJLGFBQWEsQ0FBQyxLQUFLLEdBQUcsQ0FBQyxDQUFDLEtBQUssSUFBSSxDQUFDLEVBQUUsQ0FBQztZQUN4RSxNQUFNLElBQUks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
@@ -0,0 +1,6 @@
1
+ export declare const kQueueCloseAdmission: unique symbol;
2
+ export declare const kQueueCreateItemSnapshot: unique symbol;
3
+ export declare const kQueueEnqueueRaw: unique symbol;
4
+ export declare const kQueueReserveRawAccess: unique symbol;
5
+ export declare const kQueueSetReadyItemsHandler: unique symbol;
6
+ export declare const kQueueWithRawMessage: unique symbol;
@@ -0,0 +1,7 @@
1
+ export const kQueueCloseAdmission = Symbol('smartmta.queue.closeAdmission');
2
+ export const kQueueCreateItemSnapshot = Symbol('smartmta.queue.createItemSnapshot');
3
+ export const kQueueEnqueueRaw = Symbol('smartmta.queue.enqueueRaw');
4
+ export const kQueueReserveRawAccess = Symbol('smartmta.queue.reserveRawAccess');
5
+ export const kQueueSetReadyItemsHandler = Symbol('smartmta.queue.setReadyItemsHandler');
6
+ export const kQueueWithRawMessage = Symbol('smartmta.queue.withRawMessage');
7
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3ltYm9scy5kZWxpdmVyeS5xdWV1ZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3RzL21haWwvZGVsaXZlcnkvc3ltYm9scy5kZWxpdmVyeS5xdWV1ZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxNQUFNLENBQUMsTUFBTSxvQkFBb0IsR0FBa0IsTUFBTSxDQUFDLCtCQUErQixDQUFDLENBQUM7QUFDM0YsTUFBTSxDQUFDLE1BQU0sd0JBQXdCLEdBQWtCLE1BQU0sQ0FBQyxtQ0FBbUMsQ0FBQyxDQUFDO0FBQ25HLE1BQU0sQ0FBQyxNQUFNLGdCQUFnQixHQUFrQixNQUFNLENBQUMsMkJBQTJCLENBQUMsQ0FBQztBQUNuRixNQUFNLENBQUMsTUFBTSxzQkFBc0IsR0FBa0IsTUFBTSxDQUFDLGlDQUFpQyxDQUFDLENBQUM7QUFDL0YsTUFBTSxDQUFDLE1BQU0sMEJBQTBCLEdBQWtCLE1BQU0sQ0FBQyxxQ0FBcUMsQ0FBQyxDQUFDO0FBQ3ZHLE1BQU0sQ0FBQyxNQUFNLG9CQUFvQixHQUFrQixNQUFNLENBQUMsK0JBQStCLENBQUMsQ0FBQyJ9
@@ -2,6 +2,7 @@ export * from './interfaces.storage.js';
2
2
  export * from './routing/index.js';
3
3
  export * from './security/index.js';
4
4
  export type { ISmtpTransactionAttempt } from './delivery/interfaces.js';
5
+ export type { IRawSmtpEnvelope } from './delivery/functions.raw-email.js';
5
6
  import * as Core from './core/index.js';
6
7
  import * as Delivery from './delivery/index.js';
7
8
  export { Core, Delivery };
@@ -10,4 +10,4 @@ export { Core, Delivery };
10
10
  import { Email } from './core/classes.email.js';
11
11
  // Re-export commonly used classes
12
12
  export { Email, };
13
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9tYWlsL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLGlEQUFpRDtBQUNqRCxjQUFjLHlCQUF5QixDQUFDO0FBQ3hDLGNBQWMsb0JBQW9CLENBQUM7QUFDbkMsY0FBYyxxQkFBcUIsQ0FBQztBQUdwQyxnREFBZ0Q7QUFDaEQsT0FBTyxLQUFLLElBQUksTUFBTSxpQkFBaUIsQ0FBQztBQUN4QyxPQUFPLEtBQUssUUFBUSxNQUFNLHFCQUFxQixDQUFDO0FBRWhELE9BQU8sRUFBRSxJQUFJLEVBQUUsUUFBUSxFQUFFLENBQUM7QUFFMUIscUJBQXFCO0FBQ3JCLE9BQU8sRUFBRSxLQUFLLEVBQUUsTUFBTSx5QkFBeUIsQ0FBQztBQUVoRCxrQ0FBa0M7QUFDbEMsT0FBTyxFQUNMLEtBQUssR0FDTixDQUFDIn0=
13
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9tYWlsL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLGlEQUFpRDtBQUNqRCxjQUFjLHlCQUF5QixDQUFDO0FBQ3hDLGNBQWMsb0JBQW9CLENBQUM7QUFDbkMsY0FBYyxxQkFBcUIsQ0FBQztBQUlwQyxnREFBZ0Q7QUFDaEQsT0FBTyxLQUFLLElBQUksTUFBTSxpQkFBaUIsQ0FBQztBQUN4QyxPQUFPLEtBQUssUUFBUSxNQUFNLHFCQUFxQixDQUFDO0FBRWhELE9BQU8sRUFBRSxJQUFJLEVBQUUsUUFBUSxFQUFFLENBQUM7QUFFMUIscUJBQXFCO0FBQ3JCLE9BQU8sRUFBRSxLQUFLLEVBQUUsTUFBTSx5QkFBeUIsQ0FBQztBQUVoRCxrQ0FBa0M7QUFDbEMsT0FBTyxFQUNMLEtBQUssR0FDTixDQUFDIn0=
@@ -2,7 +2,7 @@ import { EventEmitter } from 'events';
2
2
  import { DKIMCreator } from '../security/classes.dkimcreator.js';
3
3
  import type { IStorageManager } from '../interfaces.storage.js';
4
4
  import type { IInboundEmailSecurityResult } from '../../security/classes.rustsecuritybridge.js';
5
- import type { IEmailRoute, IEmailAction, IEmailDomainConfig, ISmtpAuthAccount } from './interfaces.js';
5
+ import type { IEmailRoute, IEmailAction, IEmailDomainConfig, ISmtpAuthAccount, ISmtpAuthenticateContext, ISmtpAuthenticateDecision } from './interfaces.js';
6
6
  import { Email } from '../core/classes.email.js';
7
7
  import { DomainRegistry } from './classes.domain.registry.js';
8
8
  import { BounceType, BounceCategory } from '../core/classes.bouncemanager.js';
@@ -11,6 +11,8 @@ import { MultiModeDeliverySystem, type IDeliveryStats } from '../delivery/classe
11
11
  import { UnifiedDeliveryQueue, type IQueueItem, type IQueueOptions, type IQueueStats } from '../delivery/classes.delivery.queue.js';
12
12
  import { UnifiedRateLimiter, type IHierarchicalRateLimits } from '../delivery/classes.unified.rate.limiter.js';
13
13
  import type { EmailProcessingMode, ISmtpSession as IBaseSmtpSession } from '../delivery/interfaces.js';
14
+ import { type IRawSmtpEnvelope } from '../delivery/functions.raw-email.js';
15
+ import { kSendOutboundEmailInternal, kSendRawOutboundEmailInternal } from './symbols.unified.email.server.js';
14
16
  /** External DcRouter interface shape used by UnifiedEmailServer */
15
17
  interface DcRouter {
16
18
  storageManager: IStorageManager;
@@ -18,7 +20,7 @@ interface DcRouter {
18
20
  options?: any;
19
21
  }
20
22
  export type TRecipientRouteSource = 'explicit-route' | 'domain-policy' | 'no-route';
21
- export type TRecipientRejectionReason = 'explicit-route' | 'domain-policy' | 'no-route' | 'domain-not-hosted' | 'relay-denied' | 'recipient-hook' | 'policy-error' | 'sender-scope' | 'recipient-scope' | 'account-disabled';
23
+ export type TRecipientRejectionReason = 'explicit-route' | 'domain-policy' | 'no-route' | 'domain-not-hosted' | 'relay-denied' | 'recipient-hook' | 'policy-error' | 'sender-scope' | 'recipient-scope' | 'account-disabled' | 'auth-scope-expired';
22
24
  export interface IResolvedRecipientRoute {
23
25
  recipient: string;
24
26
  localDomain: string | null;
@@ -252,6 +254,8 @@ export interface IUnifiedEmailServerOptions {
252
254
  tlsTerminatedPorts?: number[];
253
255
  /** Timeout for RCPT policy callbacks. Defaults to 5000 ms. */
254
256
  recipientValidationTimeoutMs?: number;
257
+ /** Timeout for `hooks.onAuthenticate` callbacks. Defaults to 5000 ms. */
258
+ authenticationTimeoutMs?: number;
255
259
  /** Timeout for message acceptance callbacks. Defaults to 30000 ms. */
256
260
  messageAcceptanceTimeoutMs?: number;
257
261
  /** Maximum complete messages concurrently held across Rust and TypeScript. */
@@ -263,6 +267,24 @@ export interface IUnifiedEmailServerOptions {
263
267
  };
264
268
  };
265
269
  hooks?: {
270
+ /**
271
+ * Async credential verification for PLAIN/LOGIN, letting an embedding
272
+ * application own dynamic SMTP credentials that no static configuration
273
+ * can express.
274
+ *
275
+ * Consulted only for usernames that `auth.accounts` and `auth.users` do
276
+ * not declare: configuration always owns the usernames it lists, so a
277
+ * hook can never override or weaken a configured credential. Configuring
278
+ * the hook enables SMTP AUTH on the listener even with no accounts.
279
+ *
280
+ * The returned `scope` is enforced for that session exactly as a
281
+ * configured account's scope is. A throw or a
282
+ * `smtp.authenticationTimeoutMs` timeout fails the attempt closed.
283
+ *
284
+ * SCRAM cannot be served from this hook — see `onAuthenticate` notes in
285
+ * the readme. Hook-owned usernames authenticate over PLAIN/LOGIN only.
286
+ */
287
+ onAuthenticate?: (context: ISmtpAuthenticateContext) => Promise<ISmtpAuthenticateDecision>;
266
288
  /** Async recipient policy hook that may further restrict an accepted built-in decision. */
267
289
  onRcptTo?: (context: IRcptToPolicyContext) => Promise<ISmtpDecision>;
268
290
  /** Async message acceptance hook for envelopes with no resolved `store` or `deliver` action. */
@@ -373,13 +395,52 @@ export declare class UnifiedEmailServer extends EventEmitter {
373
395
  private smtpListenerActive;
374
396
  private smtpListenerShutdown;
375
397
  private smtpListenerRestartPromise?;
398
+ private acceptingOutboundWork;
399
+ private readonly outboundWorkFlights;
400
+ private readonly outboundAuthenticationFlights;
401
+ private stopFlight?;
376
402
  private static readonly VERIFIER_DERIVATION_CONCURRENCY;
377
403
  private static readonly VERIFIER_DERIVATION_MAX_QUEUE;
378
404
  private verifierDerivationActive;
379
405
  private verifierDerivationWaiters;
380
406
  /** Compiled scope patterns; keys are config-supplied, bounded by config size. */
381
407
  private scopePatternCache;
408
+ /**
409
+ * Scopes granted by `hooks.onAuthenticate`, keyed by the session the
410
+ * credential authenticated. The SMTP listener reports no session-close
411
+ * event, so entries are bounded by both a TTL and a hard cap rather than
412
+ * by session lifetime.
413
+ *
414
+ * Losing an entry must never widen a session, so the absence of a grant is
415
+ * not read as "unrestricted": a session authenticated as a username that
416
+ * configuration does not declare can only have come from the hook, and is
417
+ * refused with a temporary failure until it authenticates again.
418
+ */
419
+ private hookAuthenticatedSessions;
420
+ private static readonly hookAuthSessionTtlMs;
421
+ private static readonly hookAuthSessionMax;
422
+ private static readonly hookAuthSweepInterval;
423
+ private hookAuthSweepCountdown;
424
+ /** The Rust listener's fixed AUTH round-trip deadline. */
425
+ private static readonly listenerAuthDeadlineMs;
426
+ /** Leave headroom so a hook answering just in time still beats the listener. */
427
+ private static readonly authHookMaxTimeoutMs;
428
+ private static readonly authHookDefaultTimeoutMs;
429
+ private loggedAuthTimeoutClamp;
382
430
  constructor(dcRouter: DcRouter, options: IUnifiedEmailServerOptions);
431
+ /**
432
+ * Hand back every event-loop resource a partially completed construction has
433
+ * already taken ownership of, so a rejected configuration leaves the process
434
+ * exactly as it found it.
435
+ *
436
+ * Only construction-time owners belong here: the delivery queue and the
437
+ * delivery system arm their timers in `initialize()` and `start()`, which a
438
+ * failed construction never reaches, and the Rust bridge is an unstarted
439
+ * singleton until `start()` spawns it.
440
+ */
441
+ private releaseConstructionOwnedResources;
442
+ private assertAcceptingOutboundWork;
443
+ private trackOutboundWork;
383
444
  onRecipientRejected(listener: (event: IRecipientRejectedEvent) => Promise<void> | void): this;
384
445
  offRecipientRejected(listener: (event: IRecipientRejectedEvent) => Promise<void> | void): this;
385
446
  private getAdvertisedHostname;
@@ -410,6 +471,36 @@ export declare class UnifiedEmailServer extends EventEmitter {
410
471
  dkimSelector?: string;
411
472
  tlsOpportunistic?: boolean;
412
473
  }): Promise<ISmtpSendResult>;
474
+ [kSendOutboundEmailInternal](host: string, port: number, email: Email, options?: {
475
+ auth?: {
476
+ user: string;
477
+ pass: string;
478
+ };
479
+ dkimDomain?: string;
480
+ dkimSelector?: string;
481
+ tlsOpportunistic?: boolean;
482
+ }): Promise<ISmtpSendResult>;
483
+ /** Send exact RFC822 bytes through the same outbound transport path as structured mail. */
484
+ sendRawOutboundEmail(host: string, port: number, rawMessage: Buffer, envelopeArg: IRawSmtpEnvelope, options: {
485
+ auth?: {
486
+ user: string;
487
+ pass: string;
488
+ };
489
+ dkimDomain: string;
490
+ dkimSelector: string;
491
+ tlsOpportunistic?: boolean;
492
+ }): Promise<ISmtpSendResult>;
493
+ [kSendRawOutboundEmailInternal](host: string, port: number, rawMessage: Buffer, envelopeArg: IRawSmtpEnvelope, options: {
494
+ auth?: {
495
+ user: string;
496
+ pass: string;
497
+ };
498
+ dkimDomain: string;
499
+ dkimSelector: string;
500
+ tlsOpportunistic?: boolean;
501
+ }): Promise<ISmtpSendResult>;
502
+ private resolveOutboundDkim;
503
+ private sendWithOutboundTransport;
413
504
  private createOutboundAuthenticationHandle;
414
505
  /**
415
506
  * Start the unified email server
@@ -432,7 +523,11 @@ export declare class UnifiedEmailServer extends EventEmitter {
432
523
  * rather than degrading silently.
433
524
  */
434
525
  private resolveTlsTerminatedPorts;
435
- /** Whether the current options require SMTP AUTH support on the listener. */
526
+ /**
527
+ * Whether the current options require SMTP AUTH support on the listener.
528
+ * A configured `hooks.onAuthenticate` counts on its own: its whole purpose
529
+ * is to own credentials that no static account list contains.
530
+ */
436
531
  private isAuthConfigured;
437
532
  /**
438
533
  * Whether the Rust SMTP listener is currently accepting connections.
@@ -453,6 +548,7 @@ export declare class UnifiedEmailServer extends EventEmitter {
453
548
  * Stop the unified email server
454
549
  */
455
550
  stop(): Promise<void>;
551
+ private stopOnce;
456
552
  private handleRustRcptToRequest;
457
553
  private evaluateDefaultRcptToPolicy;
458
554
  private resolveRecipientPolicy;
@@ -485,6 +581,47 @@ export declare class UnifiedEmailServer extends EventEmitter {
485
581
  private getAddressDomain;
486
582
  /** Resolve the configured account for a username. Accounts win over legacy users. */
487
583
  private findAuthAccount;
584
+ /** Whether the legacy `auth.users` array declares this username. */
585
+ private hasLegacyAuthUser;
586
+ /**
587
+ * Whether configuration claims this username, for the purpose of deciding
588
+ * that `hooks.onAuthenticate` must not see it.
589
+ *
590
+ * Deliberately case-insensitive, while credential verification itself stays
591
+ * exact. Otherwise `Tenant-A` would slip past an operator's configured
592
+ * `tenant-a` — including a `tenant-a` the operator had set `enabled: false`
593
+ * — and reach a hook that may well resolve names case-insensitively.
594
+ * Configuration owning a name means owning every casing of it: a casing that
595
+ * matches no exact credential simply fails, and never reaches the hook.
596
+ */
597
+ private declaresUsername;
598
+ /**
599
+ * Hook timeout, clamped below the Rust listener's own AUTH deadline.
600
+ *
601
+ * The listener waits a fixed 5 s for the TypeScript answer
602
+ * (`rust/crates/mailer-smtp/src/connection.rs`), so a larger value here
603
+ * could never take effect: the listener would already have failed the
604
+ * attempt and reset the session while the hook was still running. Clamping
605
+ * keeps the configured number meaningful instead of silently unreachable.
606
+ */
607
+ private getAuthenticationTimeoutMs;
608
+ /**
609
+ * Enforce the hard cap oldest-first, and sweep expired grants periodically.
610
+ *
611
+ * The cap is what bounds memory, and it is O(1) amortised because Map
612
+ * preserves insertion order. The TTL sweep is hygiene on top — expiry is
613
+ * always enforced on read — so it runs every `hookAuthSweepInterval`
614
+ * insertions rather than walking the whole table on every authentication.
615
+ */
616
+ private pruneHookAuthSessions;
617
+ /** Record the scope a hook granted, replacing any earlier grant for the session. */
618
+ private rememberHookAuthGrant;
619
+ /**
620
+ * Live hook grant for a session, or undefined when none is held. The
621
+ * username must match the grant: a session may not present one username to
622
+ * the listener and inherit another's scope.
623
+ */
624
+ private findHookAuthGrant;
488
625
  private normalizeScopeAddress;
489
626
  /**
490
627
  * Glob match ('a@b.com' | '*@b.com') against a normalized address, using
@@ -496,9 +633,35 @@ export declare class UnifiedEmailServer extends EventEmitter {
496
633
  /**
497
634
  * Envelope-level scope decision for an authenticated session. Returns null
498
635
  * when the envelope is permitted (unauthenticated sessions, usernames with
499
- * no configured account, and accounts without a scope are unrestricted).
636
+ * no configured account and no `hooks.onAuthenticate`, and accounts without
637
+ * a scope are unrestricted).
500
638
  */
501
639
  private resolveAuthAccountScopeDecision;
640
+ /**
641
+ * Envelope-level scope decision for a session authenticated by
642
+ * `hooks.onAuthenticate`. Reached only for usernames no configured account
643
+ * declares, so when a hook is configured the session must hold a live
644
+ * grant; a missing grant is refused temporarily rather than treated as
645
+ * unrestricted. With no hook configured the pre-existing behaviour stands:
646
+ * legacy users and unknown usernames are unscoped.
647
+ */
648
+ private resolveHookAuthScopeDecision;
649
+ /** Temporary refusal for a session whose hook grant is not available. */
650
+ private static authScopeUnavailableDecision;
651
+ /**
652
+ * Shared sender/recipient scope enforcement. An absent scope is
653
+ * unrestricted, matching `ISmtpAuthAccount.scope`.
654
+ */
655
+ private applyScopeToEnvelope;
656
+ /**
657
+ * Sender scope in force for a session.
658
+ *
659
+ * The three outcomes are kept distinct on purpose. Collapsing `refused` into
660
+ * "no scope" would make a session whose hook grant is gone look unrestricted
661
+ * to any caller that only asks "is there a scope?", which is the opposite of
662
+ * what a missing grant means.
663
+ */
664
+ private resolveSessionSenderScope;
502
665
  /**
503
666
  * Extract every mailbox address from the parsed From header. The Email
504
667
  * abstraction keeps only the first From mailbox, which is not sufficient
@@ -583,10 +746,22 @@ export declare class UnifiedEmailServer extends EventEmitter {
583
746
  * always fail.
584
747
  */
585
748
  private handleRustAuthRequest;
749
+ /**
750
+ * Run `hooks.onAuthenticate` under the configured timeout. Any throw, any
751
+ * timeout and any non-object result fail the attempt closed — an embedder
752
+ * that cannot answer must never widen access.
753
+ */
754
+ private runHookAuthentication;
586
755
  /**
587
756
  * Handle a SCRAM credential request from the Rust SMTP server.
588
757
  * Verifier accounts return their stored fields directly; plaintext
589
758
  * credentials (accounts or legacy users) derive fields on the fly.
759
+ *
760
+ * Usernames owned by `hooks.onAuthenticate` fall through to `found: false`:
761
+ * SCRAM requires the server to hold the salt, iteration count, stored key
762
+ * and server key, and a hook that verifies an opaque credential (argon2id,
763
+ * a remote authority) cannot produce them. Those usernames authenticate
764
+ * over PLAIN/LOGIN only, which is why the hook receives the password.
590
765
  */
591
766
  private handleScramCredentialRequest;
592
767
  /**
@@ -644,6 +819,7 @@ export declare class UnifiedEmailServer extends EventEmitter {
644
819
  /**
645
820
  * Send an email through the delivery system
646
821
  */
822
+ enqueueRawEmail(rawMessage: Buffer, envelope: IRawSmtpEnvelope, idempotencyKey: string): Promise<string>;
647
823
  sendEmail(email: Email, mode?: EmailProcessingMode, route?: IEmailRoute, options?: {
648
824
  skipSuppressionCheck?: boolean;
649
825
  ipAddress?: string;