@push.rocks/smartmta 7.0.0 → 8.0.1
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 +36 -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 +11 -0
- package/dist_ts/mail/core/classes.bouncemanager.js +120 -38
- package/dist_ts/mail/core/classes.email.js +14 -12
- 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/delivery/classes.delivery.queue.d.ts +77 -2
- package/dist_ts/mail/delivery/classes.delivery.queue.js +551 -46
- 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/index.d.ts +1 -0
- package/dist_ts/mail/delivery/index.js +2 -1
- package/dist_ts/mail/delivery/interfaces.d.ts +21 -0
- package/dist_ts/mail/delivery/interfaces.js +1 -1
- package/dist_ts/mail/index.d.ts +1 -0
- package/dist_ts/mail/index.js +1 -1
- package/dist_ts/mail/routing/classes.dkim.manager.d.ts +8 -4
- package/dist_ts/mail/routing/classes.dkim.manager.js +46 -29
- package/dist_ts/mail/routing/classes.dns.manager.d.ts +5 -3
- 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 +3 -0
- package/dist_ts/mail/routing/classes.email.router.js +15 -9
- package/dist_ts/mail/routing/classes.unified.email.server.d.ts +4 -0
- package/dist_ts/mail/routing/classes.unified.email.server.js +61 -40
- package/dist_ts/mail/security/classes.dkimcreator.d.ts +7 -0
- package/dist_ts/mail/security/classes.dkimcreator.js +33 -8
- package/dist_ts/mail/security/classes.spfverifier.js +5 -3
- package/dist_ts/security/classes.contentscanner.js +14 -11
- package/dist_ts/security/classes.ipreputationchecker.d.ts +3 -0
- package/dist_ts/security/classes.ipreputationchecker.js +20 -11
- package/dist_ts/security/classes.rustsecuritybridge.d.ts +49 -1
- 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 -8
- package/readme.hints.md +4 -3
- package/readme.md +41 -13
- 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 +157 -45
- package/ts/mail/core/classes.email.ts +19 -13
- package/ts/mail/core/classes.emailvalidator.ts +9 -7
- package/ts/mail/delivery/classes.delivery.queue.ts +740 -58
- 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/index.ts +1 -0
- package/ts/mail/delivery/interfaces.ts +27 -1
- package/ts/mail/index.ts +1 -0
- package/ts/mail/routing/classes.dkim.manager.ts +62 -37
- package/ts/mail/routing/classes.dns.manager.ts +36 -13
- package/ts/mail/routing/classes.email.action.executor.ts +64 -17
- package/ts/mail/routing/classes.email.router.ts +15 -8
- package/ts/mail/routing/classes.unified.email.server.ts +90 -44
- package/ts/mail/security/classes.dkimcreator.ts +50 -7
- package/ts/mail/security/classes.spfverifier.ts +4 -2
- package/ts/security/classes.contentscanner.ts +14 -11
- package/ts/security/classes.ipreputationchecker.ts +21 -10
- package/ts/security/classes.rustsecuritybridge.ts +269 -3
- package/ts/security/classes.securitylogger.ts +6 -4
- package/ts/security/index.ts +6 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as plugins from '../../plugins.js';
|
|
2
2
|
import { EventEmitter } from 'events';
|
|
3
3
|
import { logger } from '../../logger.js';
|
|
4
|
+
import { getErrorMessage } from '../../functions.errors.js';
|
|
4
5
|
import {
|
|
5
6
|
SecurityLogger,
|
|
6
7
|
SecurityLogLevel,
|
|
@@ -24,6 +25,7 @@ import { SmtpState } from '../delivery/interfaces.js';
|
|
|
24
25
|
import type { EmailProcessingMode, ISmtpSession as IBaseSmtpSession } from '../delivery/interfaces.js';
|
|
25
26
|
import { EmailActionExecutor } from './classes.email.action.executor.js';
|
|
26
27
|
import { DkimManager } from './classes.dkim.manager.js';
|
|
28
|
+
import { callObserverSafely, emitSafely } from '../delivery/functions.safe-observers.js';
|
|
27
29
|
|
|
28
30
|
|
|
29
31
|
/** External DcRouter interface shape used by UnifiedEmailServer */
|
|
@@ -104,6 +106,8 @@ export interface IUnifiedEmailServerOptions {
|
|
|
104
106
|
domains: IEmailDomainConfig[]; // Domain configurations
|
|
105
107
|
banner?: string;
|
|
106
108
|
debug?: boolean;
|
|
109
|
+
/** Automatic for standalone use; caller-managed keeps DNS/key ownership with the embedding app. */
|
|
110
|
+
dkimKeyProvisioning?: 'automatic' | 'caller-managed';
|
|
107
111
|
useSocketHandler?: boolean; // Use socket-handler mode instead of port listening
|
|
108
112
|
/** Persist router changes back into storage when a storage manager is available. */
|
|
109
113
|
persistRoutes?: boolean;
|
|
@@ -329,7 +333,16 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
329
333
|
concurrentDeliveries: 10,
|
|
330
334
|
processBounces: true,
|
|
331
335
|
bounceHandler: {
|
|
332
|
-
processSmtpFailure:
|
|
336
|
+
processSmtpFailure: async (recipient, smtpResponse, failureOptions) => {
|
|
337
|
+
const processed = await this.processSmtpFailure(
|
|
338
|
+
recipient,
|
|
339
|
+
smtpResponse,
|
|
340
|
+
failureOptions,
|
|
341
|
+
);
|
|
342
|
+
if (!processed) {
|
|
343
|
+
throw new Error(`SMTP bounce operation failed for ${recipient}`);
|
|
344
|
+
}
|
|
345
|
+
},
|
|
333
346
|
},
|
|
334
347
|
onDeliverySuccess: async (_item, _result) => {
|
|
335
348
|
// Delivery success recorded via delivery system
|
|
@@ -346,8 +359,14 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
346
359
|
});
|
|
347
360
|
|
|
348
361
|
// Initialize DKIM manager
|
|
349
|
-
this.dkimManager = new DkimManager(
|
|
350
|
-
|
|
362
|
+
this.dkimManager = new DkimManager(
|
|
363
|
+
this.dkimCreator,
|
|
364
|
+
this.domainRegistry,
|
|
365
|
+
dcRouter,
|
|
366
|
+
this.rustBridge,
|
|
367
|
+
options.dkimKeyProvisioning || 'automatic',
|
|
368
|
+
);
|
|
369
|
+
this.dnsManager = new DnsManager(this.dcRouter, undefined, options.dkimKeyProvisioning || 'automatic');
|
|
351
370
|
|
|
352
371
|
// Initialize statistics
|
|
353
372
|
this.stats = {
|
|
@@ -406,18 +425,24 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
406
425
|
tlsOpportunistic?: boolean;
|
|
407
426
|
}): Promise<ISmtpSendResult> {
|
|
408
427
|
// Build DKIM config if domain has keys
|
|
409
|
-
let dkim: {
|
|
428
|
+
let dkim: {
|
|
429
|
+
domain: string;
|
|
430
|
+
selector: string;
|
|
431
|
+
privateKey: string;
|
|
432
|
+
keyType: 'rsa' | 'ed25519';
|
|
433
|
+
} | undefined;
|
|
410
434
|
if (options?.dkimDomain) {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
435
|
+
const selector = options.dkimSelector || 'default';
|
|
436
|
+
const material = await this.dkimCreator.readValidatedDKIMKeysForSelector(
|
|
437
|
+
options.dkimDomain,
|
|
438
|
+
selector,
|
|
439
|
+
);
|
|
440
|
+
dkim = {
|
|
441
|
+
domain: options.dkimDomain,
|
|
442
|
+
selector,
|
|
443
|
+
privateKey: material.privateKey,
|
|
444
|
+
keyType: material.keyType,
|
|
445
|
+
};
|
|
421
446
|
}
|
|
422
447
|
|
|
423
448
|
// Serialize the Email to the outbound format
|
|
@@ -476,13 +501,14 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
476
501
|
await this.startDeliveryPipeline();
|
|
477
502
|
await this.startRustBridge();
|
|
478
503
|
await this.initializeDkimAndDns();
|
|
504
|
+
await this.startDeliveryProcessing();
|
|
479
505
|
this.registerBridgeEventHandlers();
|
|
480
506
|
await this.startSmtpServer();
|
|
481
507
|
this.startDnsValidation();
|
|
482
508
|
logger.log('info', 'UnifiedEmailServer started successfully');
|
|
483
|
-
this
|
|
509
|
+
await emitSafely(this, 'started');
|
|
484
510
|
} catch (error) {
|
|
485
|
-
logger.log('error', `Failed to start UnifiedEmailServer: ${error
|
|
511
|
+
logger.log('error', `Failed to start UnifiedEmailServer: ${getErrorMessage(error)}`);
|
|
486
512
|
try {
|
|
487
513
|
await this.stop();
|
|
488
514
|
} catch (cleanupError) {
|
|
@@ -493,10 +519,19 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
493
519
|
}
|
|
494
520
|
|
|
495
521
|
private async startDeliveryPipeline(): Promise<void> {
|
|
496
|
-
await
|
|
522
|
+
await Promise.all([
|
|
523
|
+
this.bounceManager.initialize(),
|
|
524
|
+
this.emailRouter.initialize(),
|
|
525
|
+
]);
|
|
526
|
+
logger.log('info', 'Managed email routing and suppression state initialized');
|
|
527
|
+
|
|
528
|
+
await this.deliveryQueue.initialize({ startProcessing: false });
|
|
497
529
|
logger.log('info', 'Email delivery queue initialized');
|
|
530
|
+
}
|
|
498
531
|
|
|
532
|
+
private async startDeliveryProcessing(): Promise<void> {
|
|
499
533
|
await this.deliverySystem.start();
|
|
534
|
+
this.deliveryQueue.resume();
|
|
500
535
|
logger.log('info', 'Email delivery system started');
|
|
501
536
|
}
|
|
502
537
|
|
|
@@ -509,9 +544,9 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
509
544
|
|
|
510
545
|
this.unregisterBridgeStateHandler();
|
|
511
546
|
this.bridgeStateChangeHandler = ({ oldState, newState }: { oldState: string; newState: string }) => {
|
|
512
|
-
if (newState === 'failed') this
|
|
513
|
-
else if (newState === 'restarting') this
|
|
514
|
-
else if (newState === 'running' && oldState === 'restarting') this
|
|
547
|
+
if (newState === 'failed') void emitSafely(this, 'bridgeFailed');
|
|
548
|
+
else if (newState === 'restarting') void emitSafely(this, 'bridgeRestarting');
|
|
549
|
+
else if (newState === 'running' && oldState === 'restarting') void emitSafely(this, 'bridgeRecovered');
|
|
515
550
|
};
|
|
516
551
|
this.rustBridge.on('stateChange', this.bridgeStateChangeHandler);
|
|
517
552
|
}
|
|
@@ -520,6 +555,15 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
520
555
|
await this.dkimManager.setupDkimForDomains();
|
|
521
556
|
logger.log('info', 'DKIM configuration completed for all domains');
|
|
522
557
|
|
|
558
|
+
if (this.options.dkimKeyProvisioning === 'caller-managed') {
|
|
559
|
+
// Caller-managed mode validates only exact-selector key material. SmartMTA
|
|
560
|
+
// must not register DNS handlers, generate DNS records, rotate keys, or
|
|
561
|
+
// mutate any /email/dns/* or /email/dkim/* managed state.
|
|
562
|
+
this.applyDomainRateLimits();
|
|
563
|
+
logger.log('info', 'Caller-managed DKIM readiness validated without provisioning');
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
523
567
|
if (!this.localDnsProvisioned) {
|
|
524
568
|
await this.dnsManager.provisionLocalDnsRecords(
|
|
525
569
|
this.domainRegistry.getAllConfigs(),
|
|
@@ -767,9 +811,9 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
767
811
|
}
|
|
768
812
|
|
|
769
813
|
logger.log('info', 'UnifiedEmailServer stopped successfully');
|
|
770
|
-
this
|
|
814
|
+
await emitSafely(this, 'stopped');
|
|
771
815
|
} catch (error) {
|
|
772
|
-
logger.log('error', `Error stopping UnifiedEmailServer: ${error
|
|
816
|
+
logger.log('error', `Error stopping UnifiedEmailServer: ${getErrorMessage(error)}`);
|
|
773
817
|
throw error;
|
|
774
818
|
}
|
|
775
819
|
}
|
|
@@ -1567,18 +1611,12 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
1567
1611
|
}
|
|
1568
1612
|
}
|
|
1569
1613
|
|
|
1570
|
-
// Sign with DKIM if configured
|
|
1571
|
-
if (mode === 'mta' && route?.action.options?.mtaOptions?.dkimSign) {
|
|
1572
|
-
const domain = email.from.split('@')[1];
|
|
1573
|
-
await this.dkimManager.handleDkimSigning(email, domain, route.action.options.mtaOptions.dkimOptions?.keySelector || 'default');
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
1614
|
const id = await this.deliveryQueue.enqueue(email, mode, route);
|
|
1577
1615
|
|
|
1578
1616
|
logger.log('info', `Email queued with ID: ${id}`);
|
|
1579
1617
|
return id;
|
|
1580
1618
|
} catch (error) {
|
|
1581
|
-
logger.log('error', `Failed to send email: ${error
|
|
1619
|
+
logger.log('error', `Failed to send email: ${getErrorMessage(error)}`);
|
|
1582
1620
|
throw error;
|
|
1583
1621
|
}
|
|
1584
1622
|
}
|
|
@@ -1599,9 +1637,9 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
1599
1637
|
bounceCategory: bounceRecord.bounceCategory
|
|
1600
1638
|
});
|
|
1601
1639
|
|
|
1602
|
-
this
|
|
1640
|
+
await emitSafely(this, 'bounceProcessed', bounceRecord);
|
|
1603
1641
|
|
|
1604
|
-
SecurityLogger.getInstance().logEvent({
|
|
1642
|
+
await callObserverSafely('bounce security log', () => SecurityLogger.getInstance().logEvent({
|
|
1605
1643
|
level: SecurityLogLevel.INFO,
|
|
1606
1644
|
type: SecurityEventType.EMAIL_VALIDATION,
|
|
1607
1645
|
message: `Bounce notification processed for recipient`,
|
|
@@ -1612,7 +1650,7 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
1612
1650
|
bounceCategory: bounceRecord.bounceCategory
|
|
1613
1651
|
},
|
|
1614
1652
|
success: true
|
|
1615
|
-
});
|
|
1653
|
+
}));
|
|
1616
1654
|
|
|
1617
1655
|
return true;
|
|
1618
1656
|
} else {
|
|
@@ -1620,15 +1658,16 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
1620
1658
|
return false;
|
|
1621
1659
|
}
|
|
1622
1660
|
} catch (error) {
|
|
1623
|
-
|
|
1661
|
+
const errorMessage = getErrorMessage(error);
|
|
1662
|
+
logger.log('error', `Error processing bounce notification: ${errorMessage}`);
|
|
1624
1663
|
|
|
1625
|
-
SecurityLogger.getInstance().logEvent({
|
|
1664
|
+
await callObserverSafely('bounce failure security log', () => SecurityLogger.getInstance().logEvent({
|
|
1626
1665
|
level: SecurityLogLevel.ERROR,
|
|
1627
1666
|
type: SecurityEventType.EMAIL_VALIDATION,
|
|
1628
1667
|
message: 'Failed to process bounce notification',
|
|
1629
|
-
details: { error:
|
|
1668
|
+
details: { error: errorMessage, subject: bounceEmail.subject },
|
|
1630
1669
|
success: false
|
|
1631
|
-
});
|
|
1670
|
+
}));
|
|
1632
1671
|
|
|
1633
1672
|
return false;
|
|
1634
1673
|
}
|
|
@@ -1637,7 +1676,13 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
1637
1676
|
public async processSmtpFailure(
|
|
1638
1677
|
recipient: string,
|
|
1639
1678
|
smtpResponse: string,
|
|
1640
|
-
options: {
|
|
1679
|
+
options: {
|
|
1680
|
+
operationId?: string;
|
|
1681
|
+
sender?: string;
|
|
1682
|
+
originalEmailId?: string;
|
|
1683
|
+
statusCode?: string;
|
|
1684
|
+
headers?: Record<string, string>;
|
|
1685
|
+
} = {}
|
|
1641
1686
|
): Promise<boolean> {
|
|
1642
1687
|
logger.log('info', `Processing SMTP failure for ${recipient}: ${smtpResponse}`);
|
|
1643
1688
|
|
|
@@ -1648,9 +1693,9 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
1648
1693
|
bounceType: bounceRecord.bounceType
|
|
1649
1694
|
});
|
|
1650
1695
|
|
|
1651
|
-
this
|
|
1696
|
+
await emitSafely(this, 'bounceProcessed', bounceRecord);
|
|
1652
1697
|
|
|
1653
|
-
SecurityLogger.getInstance().logEvent({
|
|
1698
|
+
await callObserverSafely('SMTP failure security log', () => SecurityLogger.getInstance().logEvent({
|
|
1654
1699
|
level: SecurityLogLevel.INFO,
|
|
1655
1700
|
type: SecurityEventType.EMAIL_VALIDATION,
|
|
1656
1701
|
message: `SMTP failure processed for recipient`,
|
|
@@ -1662,19 +1707,20 @@ export class UnifiedEmailServer extends EventEmitter {
|
|
|
1662
1707
|
smtpResponse
|
|
1663
1708
|
},
|
|
1664
1709
|
success: true
|
|
1665
|
-
});
|
|
1710
|
+
}));
|
|
1666
1711
|
|
|
1667
1712
|
return true;
|
|
1668
1713
|
} catch (error) {
|
|
1669
|
-
|
|
1714
|
+
const errorMessage = getErrorMessage(error);
|
|
1715
|
+
logger.log('error', `Error processing SMTP failure: ${errorMessage}`);
|
|
1670
1716
|
|
|
1671
|
-
SecurityLogger.getInstance().logEvent({
|
|
1717
|
+
await callObserverSafely('SMTP failure security log', () => SecurityLogger.getInstance().logEvent({
|
|
1672
1718
|
level: SecurityLogLevel.ERROR,
|
|
1673
1719
|
type: SecurityEventType.EMAIL_VALIDATION,
|
|
1674
1720
|
message: 'Failed to process SMTP failure',
|
|
1675
|
-
details: { recipient, smtpResponse, error:
|
|
1721
|
+
details: { recipient, smtpResponse, error: errorMessage },
|
|
1676
1722
|
success: false
|
|
1677
|
-
});
|
|
1723
|
+
}));
|
|
1678
1724
|
|
|
1679
1725
|
return false;
|
|
1680
1726
|
}
|
|
@@ -14,6 +14,14 @@ export interface IDkimKeyMetadata {
|
|
|
14
14
|
keySize: number;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
export type TDkimKeyType = 'rsa' | 'ed25519';
|
|
18
|
+
|
|
19
|
+
export interface IDkimSigningMaterial {
|
|
20
|
+
privateKey: string;
|
|
21
|
+
publicKey: string;
|
|
22
|
+
keyType: TDkimKeyType;
|
|
23
|
+
}
|
|
24
|
+
|
|
17
25
|
export class DKIMCreator {
|
|
18
26
|
constructor(private readonly storageManager: IStorageManager) {}
|
|
19
27
|
|
|
@@ -86,6 +94,44 @@ export class DKIMCreator {
|
|
|
86
94
|
return { privateKey, publicKey };
|
|
87
95
|
}
|
|
88
96
|
|
|
97
|
+
public async readValidatedDKIMKeysForSelector(
|
|
98
|
+
domain: string,
|
|
99
|
+
selector: string,
|
|
100
|
+
): Promise<IDkimSigningMaterial> {
|
|
101
|
+
const keys = await this.readDKIMKeysForSelector(domain, selector);
|
|
102
|
+
let privateKeyObject: plugins.crypto.KeyObject;
|
|
103
|
+
let publicKeyObject: plugins.crypto.KeyObject;
|
|
104
|
+
try {
|
|
105
|
+
privateKeyObject = plugins.crypto.createPrivateKey(keys.privateKey);
|
|
106
|
+
publicKeyObject = plugins.crypto.createPublicKey(keys.publicKey);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Invalid DKIM key material for domain ${domain} with selector ${selector}: ${(error as Error).message}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const keyType = privateKeyObject.asymmetricKeyType;
|
|
114
|
+
if (keyType !== 'rsa' && keyType !== 'ed25519') {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`Unsupported DKIM key type for domain ${domain} with selector ${selector}: ${keyType || 'unknown'}`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
if (publicKeyObject.asymmetricKeyType !== keyType) {
|
|
120
|
+
throw new Error(`DKIM public/private key type mismatch for domain ${domain} with selector ${selector}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const derivedPublicKey = plugins.crypto.createPublicKey(keys.privateKey).export({
|
|
124
|
+
type: 'spki',
|
|
125
|
+
format: 'der',
|
|
126
|
+
}) as Buffer;
|
|
127
|
+
const suppliedPublicKey = publicKeyObject.export({ type: 'spki', format: 'der' }) as Buffer;
|
|
128
|
+
if (!derivedPublicKey.equals(suppliedPublicKey)) {
|
|
129
|
+
throw new Error(`DKIM public/private key mismatch for domain ${domain} with selector ${selector}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { ...keys, keyType };
|
|
133
|
+
}
|
|
134
|
+
|
|
89
135
|
public async createDKIMKeys(
|
|
90
136
|
keySize = 2048,
|
|
91
137
|
): Promise<{ privateKey: string; publicKey: string }> {
|
|
@@ -186,20 +232,17 @@ export class DKIMCreator {
|
|
|
186
232
|
domain: string,
|
|
187
233
|
selector: string,
|
|
188
234
|
): Promise<plugins.tsclass.network.IDnsRecord> {
|
|
189
|
-
const
|
|
190
|
-
const keyContents =
|
|
235
|
+
const material = await this.readValidatedDKIMKeysForSelector(domain, selector);
|
|
236
|
+
const keyContents = material.publicKey
|
|
191
237
|
.replace('-----BEGIN PUBLIC KEY-----', '')
|
|
192
238
|
.replace('-----END PUBLIC KEY-----', '')
|
|
193
239
|
.replace(/\s/g, '');
|
|
194
|
-
const keyAlgo = keys.privateKey.includes('ED25519') || keys.publicKey.length < 200
|
|
195
|
-
? 'ed25519'
|
|
196
|
-
: 'rsa';
|
|
197
240
|
|
|
198
241
|
return {
|
|
199
242
|
name: `${selector}._domainkey.${domain}`,
|
|
200
243
|
type: 'TXT',
|
|
201
|
-
dnsSecEnabled:
|
|
202
|
-
value: `v=DKIM1; h=sha256; k=${
|
|
244
|
+
dnsSecEnabled: false,
|
|
245
|
+
value: `v=DKIM1; h=sha256; k=${material.keyType}; p=${keyContents}`,
|
|
203
246
|
};
|
|
204
247
|
}
|
|
205
248
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { logger } from '../../logger.js';
|
|
2
|
+
import { getErrorMessage } from '../../functions.errors.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* SPF result qualifiers
|
|
@@ -116,9 +117,10 @@ export class SpfVerifier {
|
|
|
116
117
|
|
|
117
118
|
return spfRecord;
|
|
118
119
|
} catch (error) {
|
|
119
|
-
|
|
120
|
+
const errorMessage = getErrorMessage(error);
|
|
121
|
+
logger.log('error', `Error parsing SPF record: ${errorMessage}`, {
|
|
120
122
|
record,
|
|
121
|
-
error:
|
|
123
|
+
error: errorMessage
|
|
122
124
|
});
|
|
123
125
|
return null;
|
|
124
126
|
}
|
|
@@ -5,6 +5,8 @@ import type { IAttachment } from '../mail/core/classes.email.js';
|
|
|
5
5
|
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from './classes.securitylogger.js';
|
|
6
6
|
import { RustSecurityBridge } from './classes.rustsecuritybridge.js';
|
|
7
7
|
import { LRUCache } from 'lru-cache';
|
|
8
|
+
import { getErrorMessage, getErrorStack } from '../functions.errors.js';
|
|
9
|
+
import { callObserverSafely } from '../mail/delivery/functions.safe-observers.js';
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* Scan result information
|
|
@@ -179,9 +181,10 @@ export class ContentScanner {
|
|
|
179
181
|
|
|
180
182
|
return result;
|
|
181
183
|
} catch (error) {
|
|
182
|
-
|
|
184
|
+
const errorMessage = getErrorMessage(error);
|
|
185
|
+
logger.log('error', `Error scanning email: ${errorMessage}`, {
|
|
183
186
|
messageId: email.getMessageId(),
|
|
184
|
-
error: error
|
|
187
|
+
error: getErrorStack(error)
|
|
185
188
|
});
|
|
186
189
|
|
|
187
190
|
// Return a safe default with error indication
|
|
@@ -191,7 +194,7 @@ export class ContentScanner {
|
|
|
191
194
|
scannedElements: ['error'],
|
|
192
195
|
timestamp: Date.now(),
|
|
193
196
|
threatType: 'scan_error',
|
|
194
|
-
threatDetails: `Scan error: ${
|
|
197
|
+
threatDetails: `Scan error: ${errorMessage}`
|
|
195
198
|
};
|
|
196
199
|
}
|
|
197
200
|
}
|
|
@@ -300,7 +303,7 @@ export class ContentScanner {
|
|
|
300
303
|
.replace(/[\x00-\x09\x0B-\x1F\x7F-\x9F]/g, '') // Remove control chars
|
|
301
304
|
.replace(/\uFFFD/g, ''); // Remove replacement char
|
|
302
305
|
} catch (error) {
|
|
303
|
-
logger.log('warn', `Error extracting text from buffer: ${error
|
|
306
|
+
logger.log('warn', `Error extracting text from buffer: ${getErrorMessage(error)}`);
|
|
304
307
|
return '';
|
|
305
308
|
}
|
|
306
309
|
}
|
|
@@ -340,7 +343,7 @@ export class ContentScanner {
|
|
|
340
343
|
* @param result The scan result
|
|
341
344
|
*/
|
|
342
345
|
private logHighThreatFound(email: Email, result: IScanResult): void {
|
|
343
|
-
SecurityLogger.getInstance().logEvent({
|
|
346
|
+
void callObserverSafely('content scan security log', () => SecurityLogger.getInstance().logEvent({
|
|
344
347
|
level: SecurityLogLevel.ERROR,
|
|
345
348
|
type: SecurityEventType.MALWARE,
|
|
346
349
|
message: `High threat content detected in email from ${email.from} to ${email.to.join(', ')}`,
|
|
@@ -353,8 +356,8 @@ export class ContentScanner {
|
|
|
353
356
|
subject: email.subject
|
|
354
357
|
},
|
|
355
358
|
success: false,
|
|
356
|
-
domain: email.getFromDomain()
|
|
357
|
-
});
|
|
359
|
+
domain: email.getFromDomain() || undefined
|
|
360
|
+
}));
|
|
358
361
|
}
|
|
359
362
|
|
|
360
363
|
/**
|
|
@@ -363,7 +366,7 @@ export class ContentScanner {
|
|
|
363
366
|
* @param result The scan result
|
|
364
367
|
*/
|
|
365
368
|
private logThreatFound(email: Email, result: IScanResult): void {
|
|
366
|
-
SecurityLogger.getInstance().logEvent({
|
|
369
|
+
void callObserverSafely('content scan security log', () => SecurityLogger.getInstance().logEvent({
|
|
367
370
|
level: SecurityLogLevel.WARN,
|
|
368
371
|
type: SecurityEventType.SPAM,
|
|
369
372
|
message: `Suspicious content detected in email from ${email.from} to ${email.to.join(', ')}`,
|
|
@@ -376,8 +379,8 @@ export class ContentScanner {
|
|
|
376
379
|
subject: email.subject
|
|
377
380
|
},
|
|
378
381
|
success: false,
|
|
379
|
-
domain: email.getFromDomain()
|
|
380
|
-
});
|
|
382
|
+
domain: email.getFromDomain() || undefined
|
|
383
|
+
}));
|
|
381
384
|
}
|
|
382
385
|
|
|
383
386
|
/**
|
|
@@ -396,4 +399,4 @@ export class ContentScanner {
|
|
|
396
399
|
return 'high';
|
|
397
400
|
}
|
|
398
401
|
}
|
|
399
|
-
}
|
|
402
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { logger } from '../logger.js';
|
|
2
|
+
import { getErrorMessage, getErrorStack } from '../functions.errors.js';
|
|
2
3
|
import type { IStorageManager } from '../mail/interfaces.storage.js';
|
|
3
4
|
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from './classes.securitylogger.js';
|
|
4
5
|
import { RustSecurityBridge } from './classes.rustsecuritybridge.js';
|
|
5
6
|
import { LRUCache } from 'lru-cache';
|
|
7
|
+
import { callObserverSafely } from '../mail/delivery/functions.safe-observers.js';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Reputation check result information
|
|
@@ -66,6 +68,7 @@ export class IPReputationChecker {
|
|
|
66
68
|
private reputationCache: LRUCache<string, IReputationResult>;
|
|
67
69
|
private options: Required<IIPReputationOptions>;
|
|
68
70
|
private storageManager?: IStorageManager;
|
|
71
|
+
private initializationPromise: Promise<void>;
|
|
69
72
|
|
|
70
73
|
private static readonly DEFAULT_OPTIONS: Required<IIPReputationOptions> = {
|
|
71
74
|
maxCacheSize: 10000,
|
|
@@ -92,11 +95,16 @@ export class IPReputationChecker {
|
|
|
92
95
|
ttl: this.options.cacheTTL,
|
|
93
96
|
});
|
|
94
97
|
|
|
95
|
-
|
|
96
|
-
this.loadCache()
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
const hydrationPromise = this.options.persistCache
|
|
99
|
+
? this.loadCache()
|
|
100
|
+
: Promise.resolve();
|
|
101
|
+
hydrationPromise.catch(() => undefined);
|
|
102
|
+
this.initializationPromise = hydrationPromise;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Wait until the managed reputation cache has been hydrated. */
|
|
106
|
+
public async initialize(): Promise<void> {
|
|
107
|
+
await this.initializationPromise;
|
|
100
108
|
}
|
|
101
109
|
|
|
102
110
|
public static getInstance(options: IIPReputationOptions = {}, storageManager?: IStorageManager): IPReputationChecker {
|
|
@@ -111,6 +119,8 @@ export class IPReputationChecker {
|
|
|
111
119
|
*/
|
|
112
120
|
public async checkReputation(ip: string): Promise<IReputationResult> {
|
|
113
121
|
try {
|
|
122
|
+
await this.initialize();
|
|
123
|
+
|
|
114
124
|
if (!this.isValidIPAddress(ip)) {
|
|
115
125
|
logger.log('warn', `Invalid IP address format: ${ip}`);
|
|
116
126
|
return this.createErrorResult(ip, 'Invalid IP address format');
|
|
@@ -153,11 +163,12 @@ export class IPReputationChecker {
|
|
|
153
163
|
this.logReputationCheck(ip, result);
|
|
154
164
|
return result;
|
|
155
165
|
} catch (error) {
|
|
156
|
-
|
|
166
|
+
const errorMessage = getErrorMessage(error);
|
|
167
|
+
logger.log('error', `Error checking IP reputation for ${ip}: ${errorMessage}`, {
|
|
157
168
|
ip,
|
|
158
|
-
stack: error
|
|
169
|
+
stack: getErrorStack(error)
|
|
159
170
|
});
|
|
160
|
-
const errorResult = this.createErrorResult(ip,
|
|
171
|
+
const errorResult = this.createErrorResult(ip, errorMessage);
|
|
161
172
|
// Cache error results to avoid repeated failing lookups
|
|
162
173
|
this.reputationCache.set(ip, errorResult);
|
|
163
174
|
return errorResult;
|
|
@@ -187,7 +198,7 @@ export class IPReputationChecker {
|
|
|
187
198
|
logLevel = SecurityLogLevel.WARN;
|
|
188
199
|
}
|
|
189
200
|
|
|
190
|
-
SecurityLogger.getInstance().logEvent({
|
|
201
|
+
void callObserverSafely('IP reputation security log', () => SecurityLogger.getInstance().logEvent({
|
|
191
202
|
level: logLevel,
|
|
192
203
|
type: SecurityEventType.IP_REPUTATION,
|
|
193
204
|
message: `IP reputation check ${result.isSpam ? 'flagged spam' : 'completed'} for ${ip}`,
|
|
@@ -202,7 +213,7 @@ export class IPReputationChecker {
|
|
|
202
213
|
blacklists: result.blacklists
|
|
203
214
|
},
|
|
204
215
|
success: !result.isSpam
|
|
205
|
-
});
|
|
216
|
+
}));
|
|
206
217
|
}
|
|
207
218
|
|
|
208
219
|
private async saveCache(): Promise<void> {
|