@push.rocks/smartmta 9.5.0 → 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.
- package/changelog.md +27 -0
- package/dist_rust/mailer-bin_linux_amd64 +0 -0
- package/dist_rust/mailer-bin_linux_amd64.tsrust-build.json +14 -0
- package/dist_rust/mailer-bin_linux_arm64 +0 -0
- package/dist_rust/mailer-bin_linux_arm64.tsrust-build.json +14 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +2 -1
- package/dist_ts/mail/routing/classes.unified.email.server.d.ts +140 -4
- package/dist_ts/mail/routing/classes.unified.email.server.js +371 -97
- package/dist_ts/mail/routing/interfaces.d.ts +41 -0
- package/package.json +6 -6
- package/readme.md +44 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +1 -0
- package/ts/mail/routing/classes.unified.email.server.ts +452 -108
- package/ts/mail/routing/interfaces.ts +43 -0
- package/readme.hints.md +0 -45
- package/readme.plan.md +0 -30
package/changelog.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
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
|
+
|
|
3
30
|
## 2026-07-31 - 9.5.0
|
|
4
31
|
|
|
5
32
|
### Features
|
|
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.
|
|
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=
|
package/dist_ts/index.d.ts
CHANGED
package/dist_ts/index.js
CHANGED
|
@@ -16,5 +16,6 @@ export const smartMtaCapabilities = Object.freeze({
|
|
|
16
16
|
terminalLocalDeliverActions: true,
|
|
17
17
|
smtpAuthAccountsAndScopes: true,
|
|
18
18
|
durableRawOutboundQueue: true,
|
|
19
|
+
dynamicSmtpAuthenticationHook: true,
|
|
19
20
|
});
|
|
20
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
21
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxjQUFjLHlCQUF5QixDQUFDO0FBQ3hDLGNBQWMsaUJBQWlCLENBQUM7QUFDaEMsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sRUFBRSxNQUFNLGFBQWEsQ0FBQztBQUVqRCxNQUFNLENBQUMsTUFBTSxvQkFBb0IsR0FBRyxNQUFNLENBQUMsTUFBTSxDQUFDO0lBQ2hELDBCQUEwQixFQUFFLElBQUk7SUFDaEMscUJBQXFCLEVBQUUsSUFBSTtJQUMzQiwwQkFBMEIsRUFBRSxJQUFJO0lBQ2hDLHFCQUFxQixFQUFFLElBQUk7SUFDM0Isb0JBQW9CLEVBQUUsSUFBSTtJQUMxQixpQkFBaUIsRUFBRSxJQUFJO0lBQ3ZCLG1CQUFtQixFQUFFLElBQUk7SUFDekIsbUJBQW1CLEVBQUUsSUFBSTtJQUN6Qiw0QkFBNEIsRUFBRSxJQUFJO0lBQ2xDLDZCQUE2QixFQUFFLElBQUk7SUFDbkMsMkJBQTJCLEVBQUUsSUFBSTtJQUNqQyx5QkFBeUIsRUFBRSxJQUFJO0lBQy9CLHVCQUF1QixFQUFFLElBQUk7SUFDN0IsNkJBQTZCLEVBQUUsSUFBSTtDQUMzQixDQUFDLENBQUMifQ==
|
|
@@ -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';
|
|
@@ -20,7 +20,7 @@ interface DcRouter {
|
|
|
20
20
|
options?: any;
|
|
21
21
|
}
|
|
22
22
|
export type TRecipientRouteSource = 'explicit-route' | 'domain-policy' | 'no-route';
|
|
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';
|
|
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';
|
|
24
24
|
export interface IResolvedRecipientRoute {
|
|
25
25
|
recipient: string;
|
|
26
26
|
localDomain: string | null;
|
|
@@ -254,6 +254,8 @@ export interface IUnifiedEmailServerOptions {
|
|
|
254
254
|
tlsTerminatedPorts?: number[];
|
|
255
255
|
/** Timeout for RCPT policy callbacks. Defaults to 5000 ms. */
|
|
256
256
|
recipientValidationTimeoutMs?: number;
|
|
257
|
+
/** Timeout for `hooks.onAuthenticate` callbacks. Defaults to 5000 ms. */
|
|
258
|
+
authenticationTimeoutMs?: number;
|
|
257
259
|
/** Timeout for message acceptance callbacks. Defaults to 30000 ms. */
|
|
258
260
|
messageAcceptanceTimeoutMs?: number;
|
|
259
261
|
/** Maximum complete messages concurrently held across Rust and TypeScript. */
|
|
@@ -265,6 +267,24 @@ export interface IUnifiedEmailServerOptions {
|
|
|
265
267
|
};
|
|
266
268
|
};
|
|
267
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>;
|
|
268
288
|
/** Async recipient policy hook that may further restrict an accepted built-in decision. */
|
|
269
289
|
onRcptTo?: (context: IRcptToPolicyContext) => Promise<ISmtpDecision>;
|
|
270
290
|
/** Async message acceptance hook for envelopes with no resolved `store` or `deliver` action. */
|
|
@@ -385,7 +405,40 @@ export declare class UnifiedEmailServer extends EventEmitter {
|
|
|
385
405
|
private verifierDerivationWaiters;
|
|
386
406
|
/** Compiled scope patterns; keys are config-supplied, bounded by config size. */
|
|
387
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;
|
|
388
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;
|
|
389
442
|
private assertAcceptingOutboundWork;
|
|
390
443
|
private trackOutboundWork;
|
|
391
444
|
onRecipientRejected(listener: (event: IRecipientRejectedEvent) => Promise<void> | void): this;
|
|
@@ -470,7 +523,11 @@ export declare class UnifiedEmailServer extends EventEmitter {
|
|
|
470
523
|
* rather than degrading silently.
|
|
471
524
|
*/
|
|
472
525
|
private resolveTlsTerminatedPorts;
|
|
473
|
-
/**
|
|
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
|
+
*/
|
|
474
531
|
private isAuthConfigured;
|
|
475
532
|
/**
|
|
476
533
|
* Whether the Rust SMTP listener is currently accepting connections.
|
|
@@ -524,6 +581,47 @@ export declare class UnifiedEmailServer extends EventEmitter {
|
|
|
524
581
|
private getAddressDomain;
|
|
525
582
|
/** Resolve the configured account for a username. Accounts win over legacy users. */
|
|
526
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;
|
|
527
625
|
private normalizeScopeAddress;
|
|
528
626
|
/**
|
|
529
627
|
* Glob match ('a@b.com' | '*@b.com') against a normalized address, using
|
|
@@ -535,9 +633,35 @@ export declare class UnifiedEmailServer extends EventEmitter {
|
|
|
535
633
|
/**
|
|
536
634
|
* Envelope-level scope decision for an authenticated session. Returns null
|
|
537
635
|
* when the envelope is permitted (unauthenticated sessions, usernames with
|
|
538
|
-
* no configured account
|
|
636
|
+
* no configured account and no `hooks.onAuthenticate`, and accounts without
|
|
637
|
+
* a scope are unrestricted).
|
|
539
638
|
*/
|
|
540
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;
|
|
541
665
|
/**
|
|
542
666
|
* Extract every mailbox address from the parsed From header. The Email
|
|
543
667
|
* abstraction keeps only the first From mailbox, which is not sufficient
|
|
@@ -622,10 +746,22 @@ export declare class UnifiedEmailServer extends EventEmitter {
|
|
|
622
746
|
* always fail.
|
|
623
747
|
*/
|
|
624
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;
|
|
625
755
|
/**
|
|
626
756
|
* Handle a SCRAM credential request from the Rust SMTP server.
|
|
627
757
|
* Verifier accounts return their stored fields directly; plaintext
|
|
628
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.
|
|
629
765
|
*/
|
|
630
766
|
private handleScramCredentialRequest;
|
|
631
767
|
/**
|