@push.rocks/smartmta 6.5.2 → 8.0.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 +40 -0
- package/dist_rust/mailer-bin_linux_amd64 +0 -0
- package/dist_rust/mailer-bin_linux_arm64 +0 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/functions.errors.d.ts +3 -0
- package/dist_ts/functions.errors.js +8 -0
- package/dist_ts/index.d.ts +3 -0
- package/dist_ts/index.js +4 -1
- package/dist_ts/mail/core/classes.bouncemanager.d.ts +16 -12
- package/dist_ts/mail/core/classes.bouncemanager.js +146 -129
- package/dist_ts/mail/core/classes.email.js +15 -13
- package/dist_ts/mail/core/classes.emailvalidator.d.ts +3 -3
- package/dist_ts/mail/core/classes.emailvalidator.js +7 -5
- package/dist_ts/mail/core/classes.templatemanager.d.ts +10 -6
- package/dist_ts/mail/core/classes.templatemanager.js +35 -51
- package/dist_ts/mail/delivery/classes.delivery.queue.d.ts +92 -22
- package/dist_ts/mail/delivery/classes.delivery.queue.js +738 -151
- package/dist_ts/mail/delivery/classes.delivery.system.d.ts +13 -7
- package/dist_ts/mail/delivery/classes.delivery.system.js +458 -145
- package/dist_ts/mail/delivery/classes.unified.rate.limiter.js +9 -8
- package/dist_ts/mail/delivery/functions.safe-observers.d.ts +10 -0
- package/dist_ts/mail/delivery/functions.safe-observers.js +37 -0
- package/dist_ts/mail/delivery/interfaces.d.ts +21 -0
- package/dist_ts/mail/delivery/interfaces.js +1 -1
- package/dist_ts/mail/interfaces.storage.d.ts +37 -6
- package/dist_ts/mail/interfaces.storage.js +33 -3
- package/dist_ts/mail/routing/classes.dkim.manager.d.ts +10 -6
- package/dist_ts/mail/routing/classes.dkim.manager.js +47 -31
- package/dist_ts/mail/routing/classes.dns.manager.d.ts +7 -5
- package/dist_ts/mail/routing/classes.dns.manager.js +22 -11
- package/dist_ts/mail/routing/classes.email.action.executor.d.ts +2 -1
- package/dist_ts/mail/routing/classes.email.action.executor.js +45 -16
- package/dist_ts/mail/routing/classes.email.router.d.ts +5 -2
- package/dist_ts/mail/routing/classes.email.router.js +17 -12
- package/dist_ts/mail/routing/classes.unified.email.server.d.ts +12 -6
- package/dist_ts/mail/routing/classes.unified.email.server.js +69 -78
- package/dist_ts/mail/routing/interfaces.d.ts +0 -2
- package/dist_ts/mail/security/classes.dkimcreator.d.ts +22 -45
- package/dist_ts/mail/security/classes.dkimcreator.js +93 -296
- package/dist_ts/mail/security/classes.spfverifier.js +5 -3
- package/dist_ts/paths.d.ts +0 -12
- package/dist_ts/paths.js +3 -36
- package/dist_ts/plugins.d.ts +2 -5
- package/dist_ts/plugins.js +3 -6
- package/dist_ts/security/classes.contentscanner.js +14 -12
- package/dist_ts/security/classes.ipreputationchecker.d.ts +9 -6
- package/dist_ts/security/classes.ipreputationchecker.js +42 -93
- package/dist_ts/security/classes.rustsecuritybridge.d.ts +52 -4
- package/dist_ts/security/classes.rustsecuritybridge.js +201 -4
- package/dist_ts/security/classes.securitylogger.js +7 -5
- package/dist_ts/security/index.d.ts +1 -1
- package/dist_ts/security/index.js +2 -2
- package/package.json +8 -9
- package/readme.hints.md +4 -3
- package/readme.md +50 -18
- package/readme.plan.md +6 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/functions.errors.ts +8 -0
- package/ts/index.ts +3 -0
- package/ts/mail/core/classes.bouncemanager.ts +188 -155
- package/ts/mail/core/classes.email.ts +20 -14
- package/ts/mail/core/classes.emailvalidator.ts +9 -7
- package/ts/mail/core/classes.templatemanager.ts +42 -57
- package/ts/mail/delivery/classes.delivery.queue.ts +996 -185
- package/ts/mail/delivery/classes.delivery.system.ts +583 -170
- package/ts/mail/delivery/classes.unified.rate.limiter.ts +9 -8
- package/ts/mail/delivery/functions.safe-observers.ts +45 -0
- package/ts/mail/delivery/interfaces.ts +27 -1
- package/ts/mail/interfaces.storage.ts +64 -10
- package/ts/mail/routing/classes.dkim.manager.ts +65 -40
- package/ts/mail/routing/classes.dns.manager.ts +39 -16
- package/ts/mail/routing/classes.email.action.executor.ts +64 -17
- package/ts/mail/routing/classes.email.router.ts +20 -13
- package/ts/mail/routing/classes.unified.email.server.ts +107 -86
- package/ts/mail/routing/interfaces.ts +0 -2
- package/ts/mail/security/classes.dkimcreator.ts +150 -355
- package/ts/mail/security/classes.spfverifier.ts +4 -2
- package/ts/paths.ts +2 -41
- package/ts/plugins.ts +1 -6
- package/ts/security/classes.contentscanner.ts +14 -12
- package/ts/security/classes.ipreputationchecker.ts +46 -99
- package/ts/security/classes.rustsecuritybridge.ts +272 -6
- package/ts/security/classes.securitylogger.ts +6 -4
- package/ts/security/index.ts +5 -1
|
@@ -2,6 +2,7 @@ import * as plugins from '../../plugins.js';
|
|
|
2
2
|
import { EventEmitter } from 'node:events';
|
|
3
3
|
import { logger } from '../../logger.js';
|
|
4
4
|
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.js';
|
|
5
|
+
import { callObserverSafely, emitSafely } from './functions.safe-observers.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Interface for rate limit configuration
|
|
@@ -759,7 +760,7 @@ export class UnifiedRateLimiter extends EventEmitter {
|
|
|
759
760
|
|
|
760
761
|
logger.log('warn', `IP ${ip} blocked due to excessive errors (${counter.errors}/${limit})`);
|
|
761
762
|
|
|
762
|
-
SecurityLogger.getInstance().logEvent({
|
|
763
|
+
void callObserverSafely('rate-limit security log', () => SecurityLogger.getInstance().logEvent({
|
|
763
764
|
level: SecurityLogLevel.WARN,
|
|
764
765
|
type: SecurityEventType.RATE_LIMITING,
|
|
765
766
|
message: 'IP blocked due to excessive errors',
|
|
@@ -769,7 +770,7 @@ export class UnifiedRateLimiter extends EventEmitter {
|
|
|
769
770
|
limit
|
|
770
771
|
},
|
|
771
772
|
success: false
|
|
772
|
-
});
|
|
773
|
+
}));
|
|
773
774
|
|
|
774
775
|
return true;
|
|
775
776
|
}
|
|
@@ -840,7 +841,7 @@ export class UnifiedRateLimiter extends EventEmitter {
|
|
|
840
841
|
|
|
841
842
|
logger.log('warn', `IP ${ip} blocked due to excessive authentication failures (${counter.authFailures}/${limit})`);
|
|
842
843
|
|
|
843
|
-
SecurityLogger.getInstance().logEvent({
|
|
844
|
+
void callObserverSafely('rate-limit security log', () => SecurityLogger.getInstance().logEvent({
|
|
844
845
|
level: SecurityLogLevel.WARN,
|
|
845
846
|
type: SecurityEventType.AUTHENTICATION,
|
|
846
847
|
message: 'IP blocked due to excessive authentication failures',
|
|
@@ -850,7 +851,7 @@ export class UnifiedRateLimiter extends EventEmitter {
|
|
|
850
851
|
limit
|
|
851
852
|
},
|
|
852
853
|
success: false
|
|
853
|
-
});
|
|
854
|
+
}));
|
|
854
855
|
|
|
855
856
|
return true;
|
|
856
857
|
}
|
|
@@ -888,7 +889,7 @@ export class UnifiedRateLimiter extends EventEmitter {
|
|
|
888
889
|
this.stats.currentlyBlocked++;
|
|
889
890
|
|
|
890
891
|
// Emit event
|
|
891
|
-
this
|
|
892
|
+
void emitSafely(this, 'ipBlocked', {
|
|
892
893
|
ip,
|
|
893
894
|
expiry,
|
|
894
895
|
duration: duration || this.config.global.blockDuration
|
|
@@ -916,7 +917,7 @@ export class UnifiedRateLimiter extends EventEmitter {
|
|
|
916
917
|
}
|
|
917
918
|
|
|
918
919
|
// Emit event
|
|
919
|
-
this
|
|
920
|
+
void emitSafely(this, 'ipUnblocked', { ip });
|
|
920
921
|
|
|
921
922
|
logger.log('info', `IP ${ip} unblocked`);
|
|
922
923
|
}
|
|
@@ -977,7 +978,7 @@ export class UnifiedRateLimiter extends EventEmitter {
|
|
|
977
978
|
this.stats.activeCounters = this.counters.size + this.patternCounters.size + this.ipCounters.size;
|
|
978
979
|
|
|
979
980
|
// Emit statistics update
|
|
980
|
-
this
|
|
981
|
+
void emitSafely(this, 'statsUpdated', this.stats);
|
|
981
982
|
}
|
|
982
983
|
|
|
983
984
|
/**
|
|
@@ -1064,4 +1065,4 @@ export class UnifiedRateLimiter extends EventEmitter {
|
|
|
1064
1065
|
public getDomainLimits(domain: string): IRateLimitConfig | undefined {
|
|
1065
1066
|
return this.config.domains?.[domain];
|
|
1066
1067
|
}
|
|
1067
|
-
}
|
|
1068
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
|
|
3
|
+
import { logger } from '../../logger.js';
|
|
4
|
+
import { getErrorMessage } from '../../functions.errors.js';
|
|
5
|
+
|
|
6
|
+
function logObserverFailure(message: string): void {
|
|
7
|
+
try {
|
|
8
|
+
logger.log('error', message);
|
|
9
|
+
} catch {
|
|
10
|
+
// Logging is an observer too. It must not affect the primary operation.
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Notify every EventEmitter observer without allowing observer failures to
|
|
16
|
+
* change the operation that produced the notification. rawListeners() keeps
|
|
17
|
+
* EventEmitter's once wrapper semantics intact when invoked with the emitter
|
|
18
|
+
* as its receiver.
|
|
19
|
+
*/
|
|
20
|
+
export async function emitSafely(
|
|
21
|
+
emitter: EventEmitter,
|
|
22
|
+
eventName: string | symbol,
|
|
23
|
+
...args: unknown[]
|
|
24
|
+
): Promise<void> {
|
|
25
|
+
for (const rawListener of emitter.rawListeners(eventName)) {
|
|
26
|
+
try {
|
|
27
|
+
await Reflect.apply(rawListener, emitter, args);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
logObserverFailure(`Observer for ${String(eventName)} failed: ${getErrorMessage(error)}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Run a callback observer with the same failure isolation as emitter events. */
|
|
35
|
+
export async function callObserverSafely(
|
|
36
|
+
observerName: string,
|
|
37
|
+
observer: (...args: any[]) => unknown,
|
|
38
|
+
...args: unknown[]
|
|
39
|
+
): Promise<void> {
|
|
40
|
+
try {
|
|
41
|
+
await observer(...args);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
logObserverFailure(`${observerName} observer failed: ${getErrorMessage(error)}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
* SMTP and email delivery interface definitions
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import type {
|
|
6
|
+
ISmtpRecipientResult,
|
|
7
|
+
ISmtpTranscriptEntry,
|
|
8
|
+
TSmtpDeliveryPhase,
|
|
9
|
+
} from '../../security/classes.rustsecuritybridge.js';
|
|
10
|
+
|
|
11
|
+
export interface ISmtpTransactionAttempt {
|
|
12
|
+
id: string;
|
|
13
|
+
queueItemId: string;
|
|
14
|
+
queueAttempt: number;
|
|
15
|
+
targetHost: string;
|
|
16
|
+
targetPort: number;
|
|
17
|
+
recipientDomain?: string;
|
|
18
|
+
recipients: string[];
|
|
19
|
+
startedAt: string;
|
|
20
|
+
completedAt: string;
|
|
21
|
+
durationMs: number;
|
|
22
|
+
outcome: 'succeeded' | 'failed';
|
|
23
|
+
retryable: boolean;
|
|
24
|
+
errorType?: string;
|
|
25
|
+
smtpCode?: number;
|
|
26
|
+
recipientResults?: ISmtpRecipientResult[];
|
|
27
|
+
failurePhase?: TSmtpDeliveryPhase;
|
|
28
|
+
error?: string;
|
|
29
|
+
transcript: ISmtpTranscriptEntry[];
|
|
30
|
+
}
|
|
31
|
+
|
|
5
32
|
/**
|
|
6
33
|
* SMTP session state enumeration
|
|
7
34
|
*/
|
|
@@ -164,4 +191,3 @@ export interface ISmtpAuth {
|
|
|
164
191
|
*/
|
|
165
192
|
password: string;
|
|
166
193
|
}
|
|
167
|
-
|
|
@@ -1,13 +1,67 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Durable storage for SmartMTA's small textual state.
|
|
3
|
+
*
|
|
4
|
+
* Implementations are supplied by the embedding application and are expected
|
|
5
|
+
* to use SmartData, SmartBucket, or another explicitly managed backend.
|
|
6
|
+
* SmartMTA never falls back to the local filesystem.
|
|
7
|
+
*/
|
|
8
|
+
export interface IStorageManager {
|
|
9
|
+
get(key: string): Promise<string | null>;
|
|
10
|
+
set(key: string, value: string): Promise<void>;
|
|
11
|
+
list(prefix: string): Promise<string[]>;
|
|
12
|
+
delete(key: string): Promise<void>;
|
|
6
13
|
}
|
|
7
14
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
/** Blob storage for queued messages and template attachments. */
|
|
16
|
+
export interface IBlobStorageManager {
|
|
17
|
+
get(key: string): Promise<Buffer | null>;
|
|
18
|
+
set(key: string, value: Buffer): Promise<void>;
|
|
19
|
+
list(prefix: string): Promise<string[]>;
|
|
20
|
+
delete(key: string): Promise<void>;
|
|
13
21
|
}
|
|
22
|
+
|
|
23
|
+
/** In-memory small-state store for tests and explicitly non-durable runtimes. */
|
|
24
|
+
export class MemoryStorageManager implements IStorageManager {
|
|
25
|
+
private readonly entries = new Map<string, string>();
|
|
26
|
+
|
|
27
|
+
public async get(key: string): Promise<string | null> {
|
|
28
|
+
return this.entries.get(key) ?? null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public async set(key: string, value: string): Promise<void> {
|
|
32
|
+
this.entries.set(key, value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public async list(prefix: string): Promise<string[]> {
|
|
36
|
+
return [...this.entries.keys()].filter((key) => key.startsWith(prefix)).sort();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public async delete(key: string): Promise<void> {
|
|
40
|
+
this.entries.delete(key);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** In-memory blob store for tests and explicitly non-durable runtimes. */
|
|
45
|
+
export class MemoryBlobStorageManager implements IBlobStorageManager {
|
|
46
|
+
private readonly entries = new Map<string, Buffer>();
|
|
47
|
+
|
|
48
|
+
public async get(key: string): Promise<Buffer | null> {
|
|
49
|
+
const value = this.entries.get(key);
|
|
50
|
+
return value ? Buffer.from(value) : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public async set(key: string, value: Buffer): Promise<void> {
|
|
54
|
+
this.entries.set(key, Buffer.from(value));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public async list(prefix: string): Promise<string[]> {
|
|
58
|
+
return [...this.entries.keys()].filter((key) => key.startsWith(prefix)).sort();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public async delete(key: string): Promise<void> {
|
|
62
|
+
this.entries.delete(key);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** @deprecated Use IStorageManager. */
|
|
67
|
+
export type IStorageManagerLike = IStorageManager;
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { logger } from '../../logger.js';
|
|
2
|
+
import { getErrorMessage } from '../../functions.errors.js';
|
|
2
3
|
import { DKIMCreator } from '../security/classes.dkimcreator.js';
|
|
3
|
-
import {
|
|
4
|
+
import type { IStorageManager } from '../interfaces.storage.js';
|
|
4
5
|
import { DomainRegistry } from './classes.domain.registry.js';
|
|
5
|
-
import { RustSecurityBridge } from '../../security/classes.rustsecuritybridge.js';
|
|
6
6
|
import { Email } from '../core/classes.email.js';
|
|
7
|
+
import { RustSecurityBridge } from '../../security/classes.rustsecuritybridge.js';
|
|
8
|
+
|
|
9
|
+
type TDkimKeyProvisioning = 'automatic' | 'caller-managed';
|
|
7
10
|
|
|
8
11
|
/** External DcRouter interface shape used by DkimManager */
|
|
9
12
|
interface DcRouter {
|
|
10
|
-
storageManager?:
|
|
13
|
+
storageManager?: IStorageManager;
|
|
11
14
|
dnsServer?: any;
|
|
12
15
|
}
|
|
13
16
|
|
|
@@ -16,13 +19,30 @@ interface DcRouter {
|
|
|
16
19
|
*/
|
|
17
20
|
export class DkimManager {
|
|
18
21
|
private dkimKeys: Map<string, string> = new Map();
|
|
22
|
+
private dkimCreator: DKIMCreator;
|
|
23
|
+
private domainRegistry: DomainRegistry;
|
|
24
|
+
private dcRouter: DcRouter;
|
|
25
|
+
private rustBridge?: RustSecurityBridge;
|
|
26
|
+
private keyProvisioning: TDkimKeyProvisioning;
|
|
19
27
|
|
|
28
|
+
/** v7-compatible constructor shape: the fourth argument remains the Rust bridge. */
|
|
20
29
|
constructor(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
dkimCreator: DKIMCreator,
|
|
31
|
+
domainRegistry: DomainRegistry,
|
|
32
|
+
dcRouter: DcRouter,
|
|
33
|
+
rustBridgeOrProvisioning?: RustSecurityBridge | TDkimKeyProvisioning,
|
|
34
|
+
keyProvisioning: TDkimKeyProvisioning = 'automatic',
|
|
35
|
+
) {
|
|
36
|
+
this.dkimCreator = dkimCreator;
|
|
37
|
+
this.domainRegistry = domainRegistry;
|
|
38
|
+
this.dcRouter = dcRouter;
|
|
39
|
+
if (typeof rustBridgeOrProvisioning === 'string') {
|
|
40
|
+
this.keyProvisioning = rustBridgeOrProvisioning;
|
|
41
|
+
} else {
|
|
42
|
+
this.rustBridge = rustBridgeOrProvisioning;
|
|
43
|
+
this.keyProvisioning = keyProvisioning;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
26
46
|
|
|
27
47
|
async setupDkimForDomains(): Promise<void> {
|
|
28
48
|
const domainConfigs = this.domainRegistry.getAllConfigs();
|
|
@@ -36,35 +56,43 @@ export class DkimManager {
|
|
|
36
56
|
const domain = domainConfig.domain;
|
|
37
57
|
const selector = domainConfig.dkim?.selector || 'default';
|
|
38
58
|
|
|
59
|
+
let keyPair: { privateKey: string; publicKey: string };
|
|
39
60
|
try {
|
|
40
|
-
let keyPair: { privateKey: string; publicKey: string };
|
|
41
|
-
|
|
42
61
|
try {
|
|
43
|
-
keyPair =
|
|
44
|
-
? await this.dkimCreator.readDKIMKeys(domain)
|
|
45
|
-
: await this.dkimCreator.readDKIMKeysForSelector(domain, selector);
|
|
62
|
+
keyPair = await this.dkimCreator.readValidatedDKIMKeysForSelector(domain, selector);
|
|
46
63
|
logger.log('info', `Using existing DKIM keys for domain: ${domain}`);
|
|
47
|
-
} catch {
|
|
48
|
-
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (this.keyProvisioning === 'caller-managed') {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Caller-managed DKIM readiness failed for ${domain} (selector: ${selector}): ${(error as Error).message}`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
await this.dkimCreator.createAndStoreDKIMKeysForSelector(
|
|
49
71
|
domain,
|
|
50
72
|
selector,
|
|
51
73
|
domainConfig.dkim?.keySize || 2048,
|
|
52
74
|
);
|
|
53
|
-
keyPair =
|
|
54
|
-
? await this.dkimCreator.readDKIMKeys(domain)
|
|
55
|
-
: await this.dkimCreator.readDKIMKeysForSelector(domain, selector);
|
|
75
|
+
keyPair = await this.dkimCreator.readValidatedDKIMKeysForSelector(domain, selector);
|
|
56
76
|
logger.log('info', `Generated new DKIM keys for domain: ${domain}`);
|
|
57
77
|
}
|
|
58
|
-
|
|
59
|
-
this.dkimKeys.set(domain, keyPair.privateKey);
|
|
60
|
-
logger.log('info', `DKIM keys loaded for domain: ${domain} with selector: ${selector}`);
|
|
61
78
|
} catch (error) {
|
|
62
|
-
logger.log('error', `Failed to set up DKIM for domain ${domain}: ${error
|
|
79
|
+
logger.log('error', `Failed to set up DKIM for domain ${domain}: ${getErrorMessage(error)}`);
|
|
80
|
+
if (this.keyProvisioning === 'caller-managed') {
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
63
84
|
}
|
|
85
|
+
|
|
86
|
+
this.dkimKeys.set(domain, keyPair.privateKey);
|
|
87
|
+
logger.log('info', `DKIM keys loaded for domain: ${domain} with selector: ${selector}`);
|
|
64
88
|
}
|
|
65
89
|
}
|
|
66
90
|
|
|
67
91
|
async checkAndRotateDkimKeys(): Promise<void> {
|
|
92
|
+
if (this.keyProvisioning === 'caller-managed') {
|
|
93
|
+
logger.log('debug', 'Skipping automatic DKIM rotation for caller-managed keys');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
68
96
|
const domainConfigs = this.domainRegistry.getAllConfigs();
|
|
69
97
|
|
|
70
98
|
for (const domainConfig of domainConfigs) {
|
|
@@ -115,7 +143,7 @@ export class DkimManager {
|
|
|
115
143
|
|
|
116
144
|
logger.log('info', `DKIM DNS handler registered for new selector: ${newSelector}._domainkey.${domain}`);
|
|
117
145
|
|
|
118
|
-
if (
|
|
146
|
+
if (this.dcRouter.storageManager) {
|
|
119
147
|
await this.dcRouter.storageManager.set(
|
|
120
148
|
`/email/dkim/${domain}/public.key`,
|
|
121
149
|
keyPair.publicKey
|
|
@@ -131,36 +159,33 @@ export class DkimManager {
|
|
|
131
159
|
logger.log('debug', `DKIM keys for ${domain} are up to date`);
|
|
132
160
|
}
|
|
133
161
|
} catch (error) {
|
|
134
|
-
logger.log('error', `Failed to check/rotate DKIM keys for ${domain}: ${error
|
|
162
|
+
logger.log('error', `Failed to check/rotate DKIM keys for ${domain}: ${getErrorMessage(error)}`);
|
|
135
163
|
}
|
|
136
164
|
}
|
|
137
165
|
}
|
|
138
166
|
|
|
139
|
-
|
|
167
|
+
/** v7 compatibility: explicit callers may still request eager DKIM signing. */
|
|
168
|
+
public async handleDkimSigning(email: Email, domain: string, selector: string): Promise<void> {
|
|
140
169
|
try {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const keyType = privateKey.includes('ED25519') ? 'ed25519' : 'rsa';
|
|
149
|
-
|
|
150
|
-
const signResult = await this.rustBridge.signDkim({
|
|
151
|
-
rawMessage: rawEmail,
|
|
170
|
+
if (this.keyProvisioning === 'automatic') {
|
|
171
|
+
await this.dkimCreator.handleDKIMKeysForSelector(domain, selector);
|
|
172
|
+
}
|
|
173
|
+
const material = await this.dkimCreator.readValidatedDKIMKeysForSelector(domain, selector);
|
|
174
|
+
const rustBridge = this.rustBridge || RustSecurityBridge.getInstance();
|
|
175
|
+
const signResult = await rustBridge.signDkim({
|
|
176
|
+
rawMessage: email.toRFC822String(),
|
|
152
177
|
domain,
|
|
153
178
|
selector,
|
|
154
|
-
privateKey,
|
|
155
|
-
keyType,
|
|
179
|
+
privateKey: material.privateKey,
|
|
180
|
+
keyType: material.keyType,
|
|
156
181
|
});
|
|
157
|
-
|
|
158
182
|
if (signResult.header) {
|
|
159
183
|
email.addHeader('DKIM-Signature', signResult.header);
|
|
160
184
|
logger.log('info', `Successfully added DKIM signature for ${domain}`);
|
|
161
185
|
}
|
|
162
186
|
} catch (error) {
|
|
163
|
-
logger.log('error', `Failed to sign email with DKIM: ${error.message}`);
|
|
187
|
+
logger.log('error', `Failed to sign email with DKIM: ${(error as Error).message}`);
|
|
188
|
+
if (this.keyProvisioning === 'caller-managed') throw error;
|
|
164
189
|
}
|
|
165
190
|
}
|
|
166
191
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import * as plugins from '../../plugins.js';
|
|
2
2
|
import type { IEmailDomainConfig } from './interfaces.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type { IStorageManager } from '../interfaces.storage.js';
|
|
4
|
+
import type { DKIMCreator } from '../security/classes.dkimcreator.js';
|
|
4
5
|
import { logger } from '../../logger.js';
|
|
6
|
+
import { getErrorMessage } from '../../functions.errors.js';
|
|
5
7
|
/** External DcRouter interface shape used by DnsManager */
|
|
6
8
|
interface IDcRouterLike {
|
|
7
|
-
storageManager:
|
|
9
|
+
storageManager: IStorageManager;
|
|
8
10
|
dnsServer?: any;
|
|
9
11
|
options?: { dnsNsDomains?: string[]; dnsScopes?: string[] };
|
|
10
12
|
}
|
|
@@ -50,19 +52,22 @@ interface IDnsValidationRun {
|
|
|
50
52
|
*/
|
|
51
53
|
export class DnsManager {
|
|
52
54
|
private dcRouter: IDcRouterLike;
|
|
53
|
-
private storageManager:
|
|
55
|
+
private storageManager: IStorageManager;
|
|
54
56
|
private resolverFactory: () => IDnsResolver;
|
|
55
57
|
private activeResolvers = new Set<IDnsResolver>();
|
|
58
|
+
private keyProvisioning: 'automatic' | 'caller-managed';
|
|
56
59
|
private activeValidation?: IDnsValidationRun;
|
|
57
60
|
private validationRunId = 0;
|
|
58
61
|
|
|
59
62
|
constructor(
|
|
60
63
|
dcRouter: IDcRouterLike,
|
|
61
|
-
resolverFactory: () => IDnsResolver = () => new plugins.dns.promises.Resolver()
|
|
64
|
+
resolverFactory: () => IDnsResolver = () => new plugins.dns.promises.Resolver(),
|
|
65
|
+
keyProvisioning: 'automatic' | 'caller-managed' = 'automatic',
|
|
62
66
|
) {
|
|
63
67
|
this.dcRouter = dcRouter;
|
|
64
68
|
this.storageManager = dcRouter.storageManager;
|
|
65
69
|
this.resolverFactory = resolverFactory;
|
|
70
|
+
this.keyProvisioning = keyProvisioning;
|
|
66
71
|
}
|
|
67
72
|
|
|
68
73
|
/**
|
|
@@ -125,9 +130,10 @@ export class DnsManager {
|
|
|
125
130
|
logger.log('info', `DNS validation skipped for forward mode domain: ${config.domain}`);
|
|
126
131
|
}
|
|
127
132
|
|
|
128
|
-
// DKIM keys are still generated for consistency
|
|
129
133
|
result.warnings.push(
|
|
130
|
-
|
|
134
|
+
this.keyProvisioning === 'automatic'
|
|
135
|
+
? `Domain "${config.domain}" uses forward mode. DKIM keys will be generated but signing only happens if email is processed.`
|
|
136
|
+
: `Domain "${config.domain}" uses forward mode. Caller-managed DKIM keys are never generated by SmartMTA.`
|
|
131
137
|
);
|
|
132
138
|
|
|
133
139
|
return result;
|
|
@@ -233,7 +239,7 @@ export class DnsManager {
|
|
|
233
239
|
return result;
|
|
234
240
|
}
|
|
235
241
|
result.warnings.push(
|
|
236
|
-
`Could not verify NS delegation for ${config.domain}: ${error
|
|
242
|
+
`Could not verify NS delegation for ${config.domain}: ${getErrorMessage(error)}`
|
|
237
243
|
);
|
|
238
244
|
}
|
|
239
245
|
|
|
@@ -305,7 +311,7 @@ export class DnsManager {
|
|
|
305
311
|
if (abortSignal?.aborted) {
|
|
306
312
|
return result;
|
|
307
313
|
}
|
|
308
|
-
result.errors.push(`DNS validation failed: ${error
|
|
314
|
+
result.errors.push(`DNS validation failed: ${getErrorMessage(error)}`);
|
|
309
315
|
result.valid = false;
|
|
310
316
|
}
|
|
311
317
|
|
|
@@ -404,7 +410,7 @@ export class DnsManager {
|
|
|
404
410
|
return nsRecords;
|
|
405
411
|
} catch (error) {
|
|
406
412
|
if (!abortSignal?.aborted) {
|
|
407
|
-
logger.log('warn', `Failed to resolve NS records for ${domain}: ${error
|
|
413
|
+
logger.log('warn', `Failed to resolve NS records for ${domain}: ${getErrorMessage(error)}`);
|
|
408
414
|
}
|
|
409
415
|
return [];
|
|
410
416
|
} finally {
|
|
@@ -449,7 +455,10 @@ export class DnsManager {
|
|
|
449
455
|
}
|
|
450
456
|
|
|
451
457
|
/** Provision deterministic local DNS records without performing network lookups. */
|
|
452
|
-
async provisionLocalDnsRecords(
|
|
458
|
+
async provisionLocalDnsRecords(
|
|
459
|
+
domainConfigs: IEmailDomainConfig[],
|
|
460
|
+
dkimCreator?: DKIMCreator,
|
|
461
|
+
): Promise<void> {
|
|
453
462
|
logger.log('info', `Provisioning local DNS records for ${domainConfigs.length} domains`);
|
|
454
463
|
|
|
455
464
|
const internalDnsDomains = domainConfigs.filter(config => config.dnsMode === 'internal-dns');
|
|
@@ -458,7 +467,7 @@ export class DnsManager {
|
|
|
458
467
|
|
|
459
468
|
// Create DKIM records if DKIMCreator is provided
|
|
460
469
|
if (dkimCreator) {
|
|
461
|
-
await this.createDkimRecords(
|
|
470
|
+
await this.createDkimRecords(internalDnsDomains, dkimCreator);
|
|
462
471
|
}
|
|
463
472
|
}
|
|
464
473
|
}
|
|
@@ -548,7 +557,10 @@ export class DnsManager {
|
|
|
548
557
|
* Ensure DNS records and validate public DNS state.
|
|
549
558
|
* Prefer the split lifecycle methods when validation must not block readiness.
|
|
550
559
|
*/
|
|
551
|
-
async ensureDnsRecords(
|
|
560
|
+
async ensureDnsRecords(
|
|
561
|
+
domainConfigs: IEmailDomainConfig[],
|
|
562
|
+
dkimCreator?: DKIMCreator,
|
|
563
|
+
): Promise<void> {
|
|
552
564
|
await this.provisionLocalDnsRecords(domainConfigs, dkimCreator);
|
|
553
565
|
await this.validateAndReportDnsRecords(domainConfigs);
|
|
554
566
|
}
|
|
@@ -658,7 +670,7 @@ export class DnsManager {
|
|
|
658
670
|
- DKIM: Will be created when keys are generated`);
|
|
659
671
|
|
|
660
672
|
} catch (error) {
|
|
661
|
-
logger.log('error', `Failed to create DNS records for ${domain}: ${error
|
|
673
|
+
logger.log('error', `Failed to create DNS records for ${domain}: ${getErrorMessage(error)}`);
|
|
662
674
|
}
|
|
663
675
|
}
|
|
664
676
|
}
|
|
@@ -666,13 +678,24 @@ export class DnsManager {
|
|
|
666
678
|
/**
|
|
667
679
|
* Create DKIM DNS records for all domains
|
|
668
680
|
*/
|
|
669
|
-
private async createDkimRecords(
|
|
681
|
+
private async createDkimRecords(
|
|
682
|
+
domainConfigs: IEmailDomainConfig[],
|
|
683
|
+
dkimCreator: DKIMCreator,
|
|
684
|
+
): Promise<void> {
|
|
670
685
|
for (const domainConfig of domainConfigs) {
|
|
671
686
|
const domain = domainConfig.domain;
|
|
672
687
|
const selector = domainConfig.dkim?.selector || 'default';
|
|
673
688
|
|
|
674
689
|
try {
|
|
675
|
-
|
|
690
|
+
if (this.keyProvisioning === 'caller-managed') {
|
|
691
|
+
// Ownership remains entirely with the embedding application. This is
|
|
692
|
+
// a read-only validity check: no handler, storage record, or selector
|
|
693
|
+
// mutation is permitted in caller-managed mode.
|
|
694
|
+
await dkimCreator.getDNSRecordForSelector(domain, selector);
|
|
695
|
+
logger.log('info', `Verified caller-managed DKIM record for ${selector}._domainkey.${domain}`);
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
|
|
676
699
|
const dnsRecord = await dkimCreator.getDNSRecordForDomain(domain, selector);
|
|
677
700
|
|
|
678
701
|
// For internal-dns domains, register the DNS handler
|
|
@@ -711,7 +734,7 @@ export class DnsManager {
|
|
|
711
734
|
}
|
|
712
735
|
|
|
713
736
|
} catch (error) {
|
|
714
|
-
logger.log('warn', `Could not create DKIM DNS record for ${domain}: ${error
|
|
737
|
+
logger.log('warn', `Could not create DKIM DNS record for ${domain}: ${getErrorMessage(error)}`);
|
|
715
738
|
}
|
|
716
739
|
}
|
|
717
740
|
}
|