@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.
- package/changelog.md +41 -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 +2 -0
- package/dist_ts/index.js +3 -1
- package/dist_ts/mail/delivery/classes.delivery.queue.d.ts +63 -2
- package/dist_ts/mail/delivery/classes.delivery.queue.js +749 -193
- package/dist_ts/mail/delivery/classes.delivery.system.d.ts +6 -0
- package/dist_ts/mail/delivery/classes.delivery.system.js +146 -62
- package/dist_ts/mail/delivery/functions.raw-email.d.ts +9 -0
- package/dist_ts/mail/delivery/functions.raw-email.js +85 -0
- package/dist_ts/mail/delivery/symbols.delivery.queue.d.ts +6 -0
- package/dist_ts/mail/delivery/symbols.delivery.queue.js +7 -0
- package/dist_ts/mail/index.d.ts +1 -0
- package/dist_ts/mail/index.js +1 -1
- package/dist_ts/mail/routing/classes.unified.email.server.d.ts +180 -4
- package/dist_ts/mail/routing/classes.unified.email.server.js +592 -172
- package/dist_ts/mail/routing/interfaces.d.ts +41 -0
- package/dist_ts/mail/routing/symbols.unified.email.server.d.ts +2 -0
- package/dist_ts/mail/routing/symbols.unified.email.server.js +3 -0
- package/dist_ts/security/classes.rustsecuritybridge.d.ts +11 -0
- package/dist_ts/security/classes.rustsecuritybridge.js +1 -1
- package/package.json +6 -6
- package/readme.md +78 -4
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +2 -0
- package/ts/mail/delivery/classes.delivery.queue.ts +923 -226
- package/ts/mail/delivery/classes.delivery.system.ts +192 -80
- package/ts/mail/delivery/functions.raw-email.ts +111 -0
- package/ts/mail/delivery/symbols.delivery.queue.ts +6 -0
- package/ts/mail/index.ts +1 -0
- package/ts/mail/routing/classes.unified.email.server.ts +780 -192
- package/ts/mail/routing/interfaces.ts +43 -0
- package/ts/mail/routing/symbols.unified.email.server.ts +6 -0
- package/ts/security/classes.rustsecuritybridge.ts +6 -0
- package/readme.hints.md +0 -37
- 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
|
package/dist_ts/mail/index.d.ts
CHANGED
|
@@ -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 };
|
package/dist_ts/mail/index.js
CHANGED
|
@@ -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,
|
|
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
|
-
/**
|
|
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
|
|
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;
|