@push.rocks/smartmta 9.0.0 → 9.1.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.
@@ -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 } from './interfaces.js';
5
+ import type { IEmailRoute, IEmailAction, IEmailDomainConfig, ISmtpAuthAccount } 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';
@@ -18,7 +18,7 @@ interface DcRouter {
18
18
  options?: any;
19
19
  }
20
20
  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';
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';
22
22
  export interface IResolvedRecipientRoute {
23
23
  recipient: string;
24
24
  localDomain: string | null;
@@ -207,6 +207,11 @@ export interface IUnifiedEmailServerOptions {
207
207
  username: string;
208
208
  password: string;
209
209
  }>;
210
+ /**
211
+ * Authenticated SMTP accounts with hashed credentials and send scopes.
212
+ * Coexists with legacy `users`; accounts win on username collision.
213
+ */
214
+ accounts?: ISmtpAuthAccount[];
210
215
  };
211
216
  tls?: {
212
217
  certPem?: string;
@@ -346,6 +351,15 @@ export declare class UnifiedEmailServer extends EventEmitter {
346
351
  private acceptedEnvelopeDispatchCleanupTimer?;
347
352
  private acceptedEnvelopeDispatchCleanupPromise?;
348
353
  private acceptedEnvelopeDispatchCleanupCursor?;
354
+ private smtpListenerActive;
355
+ private smtpListenerShutdown;
356
+ private smtpListenerRestartPromise?;
357
+ private static readonly VERIFIER_DERIVATION_CONCURRENCY;
358
+ private static readonly VERIFIER_DERIVATION_MAX_QUEUE;
359
+ private verifierDerivationActive;
360
+ private verifierDerivationWaiters;
361
+ /** Compiled scope patterns; keys are config-supplied, bounded by config size. */
362
+ private scopePatternCache;
349
363
  constructor(dcRouter: DcRouter, options: IUnifiedEmailServerOptions);
350
364
  onRecipientRejected(listener: (event: IRecipientRejectedEvent) => Promise<void> | void): this;
351
365
  offRecipientRejected(listener: (event: IRecipientRejectedEvent) => Promise<void> | void): this;
@@ -391,6 +405,23 @@ export declare class UnifiedEmailServer extends EventEmitter {
391
405
  private unregisterBridgeStateHandler;
392
406
  private unregisterBridgeEventHandlers;
393
407
  private startSmtpServer;
408
+ /** Whether the current options require SMTP AUTH support on the listener. */
409
+ private isAuthConfigured;
410
+ /**
411
+ * Whether the Rust SMTP listener is currently accepting connections.
412
+ * Embedders should surface this next to getBridgeState() in health
413
+ * endpoints: a failed listener reconfiguration leaves it false.
414
+ */
415
+ isSmtpListenerActive(): boolean;
416
+ /**
417
+ * Restart only the Rust SMTP listener so listener-level configuration such
418
+ * as AUTH availability takes effect without a full process restart.
419
+ * Restarts are serialized and become no-ops once shutdown has begun; a
420
+ * failed restart leaves the listener inactive, logs loudly, and emits
421
+ * 'smtpListenerRestartFailed' so embedders can react (e.g. by calling
422
+ * stop()/start()).
423
+ */
424
+ private scheduleSmtpListenerRestart;
394
425
  /**
395
426
  * Stop the unified email server
396
427
  */
@@ -402,6 +433,39 @@ export declare class UnifiedEmailServer extends EventEmitter {
402
433
  private notifyRecipientRejected;
403
434
  private buildPolicySession;
404
435
  private getAddressDomain;
436
+ /** Resolve the configured account for a username. Accounts win over legacy users. */
437
+ private findAuthAccount;
438
+ private normalizeScopeAddress;
439
+ /**
440
+ * Glob match ('a@b.com' | '*@b.com') against a normalized address, using
441
+ * the same pattern semantics as route matching. Compiled patterns are
442
+ * cached; keys are configuration-supplied (never remote-controlled), so
443
+ * the cache is bounded by account configuration size.
444
+ */
445
+ private matchesScopePatterns;
446
+ /**
447
+ * Envelope-level scope decision for an authenticated session. Returns null
448
+ * when the envelope is permitted (unauthenticated sessions, usernames with
449
+ * no configured account, and accounts without a scope are unrestricted).
450
+ */
451
+ private resolveAuthAccountScopeDecision;
452
+ /**
453
+ * Extract every mailbox address from the parsed From header. The Email
454
+ * abstraction keeps only the first From mailbox, which is not sufficient
455
+ * for scope enforcement: `From: allowed@a.com, victim@b.com` must not
456
+ * pass. Parsing stops at the first CRLFCRLF header boundary when present
457
+ * (the Rust receiver normalizes line endings to CRLF); a bodyless message
458
+ * without one is parsed whole.
459
+ */
460
+ private parseHeaderFromMailboxes;
461
+ /**
462
+ * Header-From alignment for scoped authenticated accounts at DATA. The
463
+ * envelope MAIL FROM is enforced per recipient via resolveRecipientPolicy
464
+ * from session.envelope truth; every parsed header From mailbox must
465
+ * independently stay in scope because route matching and downstream
466
+ * consumers see the header identity.
467
+ */
468
+ private checkAuthenticatedHeaderFromScope;
405
469
  private getRecipientValidationTimeoutMs;
406
470
  private isRecipientValidationEnabled;
407
471
  private getMessageAcceptanceTimeoutMs;
@@ -440,12 +504,39 @@ export declare class UnifiedEmailServer extends EventEmitter {
440
504
  private isStandardParsedHeader;
441
505
  private stringifyParsedHeaderValue;
442
506
  /**
443
- * Handle an authRequest event from the Rust SMTP server.
507
+ * Compute SCRAM-SHA-256 credential fields from a plaintext password with a
508
+ * fresh random salt (used for legacy plaintext credentials only; verifier
509
+ * accounts return their stored fields directly).
510
+ */
511
+ private computeScramCredentials;
512
+ /**
513
+ * Bound concurrent PBKDF2 derivations so credential-stuffing pressure at
514
+ * high configured iteration counts cannot exhaust the shared libuv
515
+ * threadpool. Returns false when the wait queue is saturated (fail fast).
516
+ */
517
+ private acquireVerifierDerivationSlot;
518
+ private releaseVerifierDerivationSlot;
519
+ /**
520
+ * Verify a presented plaintext password against a stored SCRAM verifier by
521
+ * deriving StoredKey from the presented password and comparing in constant
522
+ * time. PBKDF2 runs on the async crypto threadpool so caller-configured
523
+ * iteration counts never block the event loop inside the Rust auth
524
+ * callback window; iteration bounds are enforced at configuration time and
525
+ * in-flight derivations are bounded by a small semaphore.
526
+ */
527
+ private verifyPasswordAgainstScramVerifier;
528
+ /** Constant-time string comparison for legacy plaintext credentials. */
529
+ private timingSafeStringEqual;
530
+ /**
531
+ * Handle an authRequest event from the Rust SMTP server (PLAIN/LOGIN).
532
+ * Accounts win over legacy users on username collision; disabled accounts
533
+ * always fail.
444
534
  */
445
535
  private handleRustAuthRequest;
446
536
  /**
447
537
  * Handle a SCRAM credential request from the Rust SMTP server.
448
- * Computes SCRAM-SHA-256 credentials from the stored password for the given user.
538
+ * Verifier accounts return their stored fields directly; plaintext
539
+ * credentials (accounts or legacy users) derive fields on the fly.
449
540
  */
450
541
  private handleScramCredentialRequest;
451
542
  /**