@serve.zone/dcrouter 13.17.9 → 13.19.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/dist_serve/bundle.js +6 -5
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +9 -5
- package/dist_ts/classes.dcrouter.js +152 -120
- package/dist_ts/config/classes.route-config-manager.d.ts +13 -5
- package/dist_ts/config/classes.route-config-manager.js +76 -36
- package/dist_ts/db/documents/classes.route.doc.d.ts +2 -0
- package/dist_ts/db/documents/classes.route.doc.js +11 -2
- package/dist_ts/email/classes.email-domain.manager.d.ts +7 -0
- package/dist_ts/email/classes.email-domain.manager.js +118 -55
- package/dist_ts/email/classes.smartmta-storage-manager.d.ts +13 -0
- package/dist_ts/email/classes.smartmta-storage-manager.js +101 -0
- package/dist_ts/email/email-dns-records.d.ts +14 -0
- package/dist_ts/email/email-dns-records.js +34 -0
- package/dist_ts/email/index.d.ts +2 -0
- package/dist_ts/email/index.js +3 -1
- package/dist_ts/opsserver/handlers/email-ops.handler.js +6 -15
- package/dist_ts/opsserver/handlers/route-management.handler.js +5 -7
- package/dist_ts/opsserver/handlers/stats.handler.js +41 -7
- package/dist_ts_interfaces/data/route-management.d.ts +2 -0
- package/dist_ts_migrations/index.js +25 -1
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate.js +13 -4
- package/dist_ts_web/elements/network/ops-view-routes.d.ts +2 -0
- package/dist_ts_web/elements/network/ops-view-routes.js +44 -21
- package/package.json +2 -2
- package/readme.md +190 -1543
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +190 -138
- package/ts/config/classes.route-config-manager.ts +97 -42
- package/ts/db/documents/classes.route.doc.ts +7 -0
- package/ts/email/classes.email-domain.manager.ts +136 -51
- package/ts/email/classes.smartmta-storage-manager.ts +108 -0
- package/ts/email/email-dns-records.ts +53 -0
- package/ts/email/index.ts +2 -0
- package/ts/opsserver/handlers/email-ops.handler.ts +5 -19
- package/ts/opsserver/handlers/route-management.handler.ts +4 -6
- package/ts/opsserver/handlers/stats.handler.ts +43 -7
- package/ts_apiclient/readme.md +69 -195
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate.ts +16 -4
- package/ts_web/elements/network/ops-view-routes.ts +47 -29
- package/ts_web/readme.md +41 -242
package/ts/00_commitinfo_data.ts
CHANGED
package/ts/classes.dcrouter.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type IUnifiedEmailServerOptions,
|
|
10
10
|
type IEmailRoute,
|
|
11
11
|
type IEmailDomainConfig,
|
|
12
|
+
type IStorageManagerLike,
|
|
12
13
|
} from '@push.rocks/smartmta';
|
|
13
14
|
import { logger } from './logger.js';
|
|
14
15
|
import { StorageBackedCertManager } from './classes.storage-cert-manager.js';
|
|
@@ -29,7 +30,8 @@ import { SecurityLogger, ContentScanner, IPReputationChecker } from './security/
|
|
|
29
30
|
import { type IHttp3Config, augmentRoutesWithHttp3 } from './http3/index.js';
|
|
30
31
|
import { DnsManager } from './dns/manager.dns.js';
|
|
31
32
|
import { AcmeConfigManager } from './acme/manager.acme-config.js';
|
|
32
|
-
import { EmailDomainManager } from './email/
|
|
33
|
+
import { EmailDomainManager, SmartMtaStorageManager, buildEmailDnsRecords } from './email/index.js';
|
|
34
|
+
import type { IRoute } from '../ts_interfaces/data/route-management.js';
|
|
33
35
|
|
|
34
36
|
export interface IDcRouterOptions {
|
|
35
37
|
/** Base directory for all dcrouter data. Defaults to ~/.serve.zone/dcrouter */
|
|
@@ -248,15 +250,13 @@ export class DcRouter {
|
|
|
248
250
|
public radiusServer?: RadiusServer;
|
|
249
251
|
public opsServer!: OpsServer;
|
|
250
252
|
public metricsManager?: MetricsManager;
|
|
253
|
+
private emailEventSubscriptions: Array<{
|
|
254
|
+
emitter: { off(eventName: string, listener: (...args: any[]) => void): void };
|
|
255
|
+
eventName: string;
|
|
256
|
+
listener: (...args: any[]) => void;
|
|
257
|
+
}> = [];
|
|
251
258
|
|
|
252
|
-
|
|
253
|
-
public storageManager: any = {
|
|
254
|
-
get: async (_key: string) => null,
|
|
255
|
-
set: async (_key: string, _value: string) => {
|
|
256
|
-
// DKIM keys from smartmta — logged but not yet migrated to smartdata
|
|
257
|
-
logger.log('debug', `storageManager.set() called (compat shim) for key: ${_key}`);
|
|
258
|
-
},
|
|
259
|
-
};
|
|
259
|
+
public storageManager: IStorageManagerLike;
|
|
260
260
|
|
|
261
261
|
// Unified database (smartdata + LocalSmartDb or external MongoDB)
|
|
262
262
|
public dcRouterDb?: DcRouterDb;
|
|
@@ -315,7 +315,8 @@ export class DcRouter {
|
|
|
315
315
|
// Seed routes assembled during setupSmartProxy, passed to RouteConfigManager for DB seeding
|
|
316
316
|
private seedConfigRoutes: plugins.smartproxy.IRouteConfig[] = [];
|
|
317
317
|
private seedEmailRoutes: plugins.smartproxy.IRouteConfig[] = [];
|
|
318
|
-
|
|
318
|
+
private seedDnsRoutes: plugins.smartproxy.IRouteConfig[] = [];
|
|
319
|
+
// Live DoH routes used during SmartProxy bootstrap before RouteConfigManager re-applies stored routes.
|
|
319
320
|
private runtimeDnsRoutes: plugins.smartproxy.IRouteConfig[] = [];
|
|
320
321
|
|
|
321
322
|
// Environment access
|
|
@@ -329,6 +330,10 @@ export class DcRouter {
|
|
|
329
330
|
|
|
330
331
|
// Resolve all data paths from baseDir
|
|
331
332
|
this.resolvedPaths = paths.resolvePaths(this.options.baseDir);
|
|
333
|
+
paths.ensureDataDirectories(this.resolvedPaths);
|
|
334
|
+
this.storageManager = new SmartMtaStorageManager(
|
|
335
|
+
plugins.path.join(this.resolvedPaths.dataDir, 'smartmta-storage')
|
|
336
|
+
);
|
|
332
337
|
|
|
333
338
|
// Initialize service manager and register all services
|
|
334
339
|
this.serviceManager = new plugins.taskbuffer.ServiceManager({
|
|
@@ -452,9 +457,13 @@ export class DcRouter {
|
|
|
452
457
|
.dependsOn('DcRouterDb')
|
|
453
458
|
.withStart(async () => {
|
|
454
459
|
this.emailDomainManager = new EmailDomainManager(this);
|
|
460
|
+
await this.emailDomainManager.start();
|
|
455
461
|
})
|
|
456
462
|
.withStop(async () => {
|
|
457
|
-
this.emailDomainManager
|
|
463
|
+
if (this.emailDomainManager) {
|
|
464
|
+
await this.emailDomainManager.stop();
|
|
465
|
+
this.emailDomainManager = undefined;
|
|
466
|
+
}
|
|
458
467
|
}),
|
|
459
468
|
);
|
|
460
469
|
}
|
|
@@ -581,13 +590,15 @@ export class DcRouter {
|
|
|
581
590
|
this.tunnelManager.syncAllowedEdges();
|
|
582
591
|
}
|
|
583
592
|
},
|
|
584
|
-
|
|
593
|
+
undefined,
|
|
594
|
+
(storedRoute: IRoute) => this.hydrateStoredRouteForRuntime(storedRoute),
|
|
585
595
|
);
|
|
586
596
|
this.apiTokenManager = new ApiTokenManager();
|
|
587
597
|
await this.apiTokenManager.initialize();
|
|
588
598
|
await this.routeConfigManager.initialize(
|
|
589
599
|
this.seedConfigRoutes as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig[],
|
|
590
600
|
this.seedEmailRoutes as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig[],
|
|
601
|
+
this.seedDnsRoutes as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig[],
|
|
591
602
|
);
|
|
592
603
|
await this.targetProfileManager.normalizeAllRouteRefs();
|
|
593
604
|
|
|
@@ -610,19 +621,20 @@ export class DcRouter {
|
|
|
610
621
|
|
|
611
622
|
// Email Server: optional, depends on SmartProxy
|
|
612
623
|
if (this.options.emailConfig) {
|
|
624
|
+
const emailServiceDeps = ['SmartProxy', 'MetricsManager'];
|
|
625
|
+
if (this.options.dbConfig?.enabled !== false) {
|
|
626
|
+
emailServiceDeps.push('EmailDomainManager');
|
|
627
|
+
}
|
|
613
628
|
this.serviceManager.addService(
|
|
614
629
|
new plugins.taskbuffer.Service('EmailServer')
|
|
615
630
|
.optional()
|
|
616
|
-
.dependsOn(
|
|
631
|
+
.dependsOn(...emailServiceDeps)
|
|
617
632
|
.withStart(async () => {
|
|
618
633
|
await this.setupUnifiedEmailHandling();
|
|
619
634
|
})
|
|
620
635
|
.withStop(async () => {
|
|
621
636
|
if (this.emailServer) {
|
|
622
|
-
|
|
623
|
-
(this.emailServer as any).deliverySystem.removeAllListeners();
|
|
624
|
-
}
|
|
625
|
-
this.emailServer.removeAllListeners();
|
|
637
|
+
this.clearEmailEventSubscriptions();
|
|
626
638
|
await this.emailServer.stop();
|
|
627
639
|
this.emailServer = undefined;
|
|
628
640
|
}
|
|
@@ -636,7 +648,7 @@ export class DcRouter {
|
|
|
636
648
|
this.serviceManager.addService(
|
|
637
649
|
new plugins.taskbuffer.Service('DnsServer')
|
|
638
650
|
.optional()
|
|
639
|
-
.dependsOn('SmartProxy')
|
|
651
|
+
.dependsOn('SmartProxy', ...(this.options.emailConfig ? ['EmailServer'] : []))
|
|
640
652
|
.withStart(async () => {
|
|
641
653
|
await this.setupDnsWithSocketHandler();
|
|
642
654
|
})
|
|
@@ -904,10 +916,12 @@ export class DcRouter {
|
|
|
904
916
|
logger.log('debug', 'Email routes generated', { routes: JSON.stringify(this.seedEmailRoutes) });
|
|
905
917
|
}
|
|
906
918
|
|
|
919
|
+
this.seedDnsRoutes = [];
|
|
907
920
|
this.runtimeDnsRoutes = [];
|
|
908
921
|
if (this.options.dnsNsDomains && this.options.dnsNsDomains.length > 0) {
|
|
909
|
-
this.
|
|
910
|
-
|
|
922
|
+
this.seedDnsRoutes = this.generateDnsRoutes({ includeSocketHandler: false });
|
|
923
|
+
this.runtimeDnsRoutes = this.generateDnsRoutes({ includeSocketHandler: true });
|
|
924
|
+
logger.log('debug', `DNS routes for nameservers ${this.options.dnsNsDomains.join(', ')}`, { routes: JSON.stringify(this.seedDnsRoutes) });
|
|
911
925
|
}
|
|
912
926
|
|
|
913
927
|
// Combined routes for SmartProxy bootstrap (before DB routes are loaded)
|
|
@@ -1330,19 +1344,20 @@ export class DcRouter {
|
|
|
1330
1344
|
/**
|
|
1331
1345
|
* Generate SmartProxy routes for DNS configuration
|
|
1332
1346
|
*/
|
|
1333
|
-
private generateDnsRoutes(): plugins.smartproxy.IRouteConfig[] {
|
|
1347
|
+
private generateDnsRoutes(options?: { includeSocketHandler?: boolean }): plugins.smartproxy.IRouteConfig[] {
|
|
1334
1348
|
if (!this.options.dnsNsDomains || this.options.dnsNsDomains.length === 0) {
|
|
1335
1349
|
return [];
|
|
1336
1350
|
}
|
|
1337
|
-
|
|
1351
|
+
|
|
1352
|
+
const includeSocketHandler = options?.includeSocketHandler !== false;
|
|
1338
1353
|
const dnsRoutes: plugins.smartproxy.IRouteConfig[] = [];
|
|
1339
|
-
|
|
1354
|
+
|
|
1340
1355
|
// Create routes for DNS-over-HTTPS paths
|
|
1341
1356
|
const dohPaths = ['/dns-query', '/resolve'];
|
|
1342
|
-
|
|
1357
|
+
|
|
1343
1358
|
// Use the first nameserver domain for DoH routes
|
|
1344
1359
|
const primaryNameserver = this.options.dnsNsDomains[0];
|
|
1345
|
-
|
|
1360
|
+
|
|
1346
1361
|
for (const path of dohPaths) {
|
|
1347
1362
|
const dohRoute: plugins.smartproxy.IRouteConfig = {
|
|
1348
1363
|
name: `dns-over-https-${path.replace('/', '')}`,
|
|
@@ -1351,18 +1366,42 @@ export class DcRouter {
|
|
|
1351
1366
|
domains: [primaryNameserver],
|
|
1352
1367
|
path: path
|
|
1353
1368
|
},
|
|
1354
|
-
action:
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1369
|
+
action: includeSocketHandler
|
|
1370
|
+
? {
|
|
1371
|
+
type: 'socket-handler' as any,
|
|
1372
|
+
socketHandler: this.createDnsSocketHandler()
|
|
1373
|
+
} as any
|
|
1374
|
+
: {
|
|
1375
|
+
type: 'socket-handler' as any,
|
|
1376
|
+
} as any
|
|
1358
1377
|
};
|
|
1359
|
-
|
|
1378
|
+
|
|
1360
1379
|
dnsRoutes.push(dohRoute);
|
|
1361
1380
|
}
|
|
1362
|
-
|
|
1381
|
+
|
|
1363
1382
|
return dnsRoutes;
|
|
1364
1383
|
}
|
|
1365
1384
|
|
|
1385
|
+
private hydrateStoredRouteForRuntime(storedRoute: IRoute): plugins.smartproxy.IRouteConfig | undefined {
|
|
1386
|
+
const routeName = storedRoute.route.name || '';
|
|
1387
|
+
const isDohRoute = storedRoute.origin === 'dns'
|
|
1388
|
+
&& storedRoute.route.action?.type === 'socket-handler'
|
|
1389
|
+
&& routeName.startsWith('dns-over-https-');
|
|
1390
|
+
|
|
1391
|
+
if (!isDohRoute) {
|
|
1392
|
+
return undefined;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
return {
|
|
1396
|
+
...storedRoute.route,
|
|
1397
|
+
action: {
|
|
1398
|
+
...storedRoute.route.action,
|
|
1399
|
+
type: 'socket-handler' as any,
|
|
1400
|
+
socketHandler: this.createDnsSocketHandler(),
|
|
1401
|
+
} as any,
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1366
1405
|
/**
|
|
1367
1406
|
* Check if a domain matches a pattern (including wildcard support)
|
|
1368
1407
|
* @param domain The domain to check
|
|
@@ -1511,40 +1550,74 @@ export class DcRouter {
|
|
|
1511
1550
|
...this.options.emailConfig,
|
|
1512
1551
|
domains: transformedDomains,
|
|
1513
1552
|
ports: this.options.emailConfig.ports.map(port => portMapping[port] || port + 10000),
|
|
1514
|
-
|
|
1553
|
+
persistRoutes: this.options.emailConfig.persistRoutes ?? false,
|
|
1554
|
+
queue: {
|
|
1555
|
+
storageType: 'disk',
|
|
1556
|
+
persistentPath: plugins.path.join(this.resolvedPaths.dataDir, 'smartmta-queue'),
|
|
1557
|
+
...this.options.emailConfig.queue,
|
|
1558
|
+
},
|
|
1515
1559
|
};
|
|
1516
1560
|
|
|
1517
1561
|
// Create unified email server
|
|
1518
1562
|
this.emailServer = new UnifiedEmailServer(this, emailConfig);
|
|
1563
|
+
this.clearEmailEventSubscriptions();
|
|
1519
1564
|
|
|
1520
1565
|
// Set up error handling
|
|
1521
|
-
this.emailServer
|
|
1566
|
+
this.addEmailEventSubscription(this.emailServer, 'error', (err: Error) => {
|
|
1522
1567
|
logger.log('error', `UnifiedEmailServer error: ${err.message}`);
|
|
1523
1568
|
});
|
|
1524
1569
|
|
|
1525
1570
|
// Start the server
|
|
1526
1571
|
await this.emailServer.start();
|
|
1527
1572
|
|
|
1528
|
-
// Wire delivery events to MetricsManager and logger
|
|
1529
|
-
if (this.metricsManager && this.emailServer
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1573
|
+
// Wire delivery events to MetricsManager and logger using smartmta's public queue APIs.
|
|
1574
|
+
if (this.metricsManager && this.emailServer) {
|
|
1575
|
+
const getEnvelope = (item: { processingResult?: any; lastError?: string }) => {
|
|
1576
|
+
const emailLike = item?.processingResult;
|
|
1577
|
+
const from = emailLike?.from || emailLike?.email?.from || '';
|
|
1578
|
+
const recipients = Array.isArray(emailLike?.to)
|
|
1579
|
+
? emailLike.to
|
|
1580
|
+
: Array.isArray(emailLike?.email?.to)
|
|
1581
|
+
? emailLike.email.to
|
|
1582
|
+
: [];
|
|
1583
|
+
return {
|
|
1584
|
+
from,
|
|
1585
|
+
recipients: recipients.filter(Boolean),
|
|
1586
|
+
};
|
|
1587
|
+
};
|
|
1588
|
+
const updateQueueSize = () => {
|
|
1589
|
+
this.metricsManager!.updateQueueSize(this.emailServer!.getQueueStats().queueSize);
|
|
1590
|
+
};
|
|
1591
|
+
|
|
1592
|
+
this.addEmailEventSubscription(this.emailServer.deliveryQueue, 'itemEnqueued', (item: any) => {
|
|
1593
|
+
const envelope = getEnvelope(item);
|
|
1594
|
+
this.metricsManager!.trackEmailReceived(envelope.from);
|
|
1595
|
+
updateQueueSize();
|
|
1596
|
+
logger.log('info', `Email queued: ${envelope.from} → ${envelope.recipients.join(', ') || 'unknown'}`, { zone: 'email' });
|
|
1533
1597
|
});
|
|
1534
|
-
this.emailServer.
|
|
1535
|
-
|
|
1536
|
-
|
|
1598
|
+
this.addEmailEventSubscription(this.emailServer.deliveryQueue, 'itemDelivered', (item: any) => {
|
|
1599
|
+
const envelope = getEnvelope(item);
|
|
1600
|
+
this.metricsManager!.trackEmailSent(envelope.recipients[0]);
|
|
1601
|
+
updateQueueSize();
|
|
1602
|
+
logger.log('info', `Email delivered to ${envelope.recipients.join(', ') || 'unknown'}`, { zone: 'email' });
|
|
1537
1603
|
});
|
|
1538
|
-
this.emailServer.
|
|
1539
|
-
|
|
1540
|
-
|
|
1604
|
+
this.addEmailEventSubscription(this.emailServer.deliveryQueue, 'itemFailed', (item: any) => {
|
|
1605
|
+
const envelope = getEnvelope(item);
|
|
1606
|
+
this.metricsManager!.trackEmailFailed(envelope.recipients[0], item?.lastError);
|
|
1607
|
+
updateQueueSize();
|
|
1608
|
+
logger.log('warn', `Email delivery failed to ${envelope.recipients.join(', ') || 'unknown'}: ${item?.lastError || 'unknown error'}`, { zone: 'email' });
|
|
1541
1609
|
});
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1610
|
+
this.addEmailEventSubscription(this.emailServer.deliveryQueue, 'itemDeferred', () => {
|
|
1611
|
+
updateQueueSize();
|
|
1612
|
+
});
|
|
1613
|
+
this.addEmailEventSubscription(this.emailServer.deliveryQueue, 'itemRemoved', () => {
|
|
1614
|
+
updateQueueSize();
|
|
1615
|
+
});
|
|
1616
|
+
this.addEmailEventSubscription(this.emailServer, 'bounceProcessed', () => {
|
|
1545
1617
|
this.metricsManager!.trackEmailBounced();
|
|
1546
1618
|
logger.log('warn', 'Email bounce processed', { zone: 'email' });
|
|
1547
1619
|
});
|
|
1620
|
+
updateQueueSize();
|
|
1548
1621
|
}
|
|
1549
1622
|
|
|
1550
1623
|
logger.log('info', `Email server started on ports: ${emailConfig.ports.join(', ')}`);
|
|
@@ -1574,11 +1647,7 @@ export class DcRouter {
|
|
|
1574
1647
|
try {
|
|
1575
1648
|
// Stop the unified email server which contains all components
|
|
1576
1649
|
if (this.emailServer) {
|
|
1577
|
-
|
|
1578
|
-
if ((this.emailServer as any).deliverySystem) {
|
|
1579
|
-
(this.emailServer as any).deliverySystem.removeAllListeners();
|
|
1580
|
-
}
|
|
1581
|
-
this.emailServer.removeAllListeners();
|
|
1650
|
+
this.clearEmailEventSubscriptions();
|
|
1582
1651
|
await this.emailServer.stop();
|
|
1583
1652
|
logger.log('info', 'Unified email server stopped');
|
|
1584
1653
|
this.emailServer = undefined;
|
|
@@ -1783,14 +1852,14 @@ export class DcRouter {
|
|
|
1783
1852
|
// Generate and register authoritative records
|
|
1784
1853
|
const authoritativeRecords = await this.generateAuthoritativeRecords();
|
|
1785
1854
|
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1855
|
+
// Generate email DNS records
|
|
1856
|
+
const emailDnsRecords = await this.generateEmailDnsRecords();
|
|
1857
|
+
|
|
1858
|
+
// Ensure DKIM keys exist for internal-dns domains before generating records.
|
|
1859
|
+
await this.initializeDkimForEmailDomains();
|
|
1860
|
+
|
|
1861
|
+
// Generate DKIM records directly from smartmta instead of scanning legacy JSON files.
|
|
1862
|
+
const dkimRecords = await this.loadDkimRecords();
|
|
1794
1863
|
|
|
1795
1864
|
// Combine all records: authoritative, email, DKIM, and user-defined
|
|
1796
1865
|
const allRecords = [...authoritativeRecords, ...emailDnsRecords, ...dkimRecords];
|
|
@@ -1901,37 +1970,20 @@ export class DcRouter {
|
|
|
1901
1970
|
for (const domainConfig of internalDnsDomains) {
|
|
1902
1971
|
const domain = domainConfig.domain;
|
|
1903
1972
|
const ttl = domainConfig.dns?.internal?.ttl || 3600;
|
|
1904
|
-
const
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
type: 'TXT',
|
|
1919
|
-
value: spfRecord,
|
|
1920
|
-
ttl
|
|
1921
|
-
});
|
|
1922
|
-
|
|
1923
|
-
// DMARC record - using sensible defaults
|
|
1924
|
-
const dmarcPolicy = 'none'; // Start with 'none' policy for monitoring
|
|
1925
|
-
const dmarcEmail = `dmarc@${domain}`;
|
|
1926
|
-
records.push({
|
|
1927
|
-
name: `_dmarc.${domain}`,
|
|
1928
|
-
type: 'TXT',
|
|
1929
|
-
value: `v=DMARC1; p=${dmarcPolicy}; rua=mailto:${dmarcEmail}`,
|
|
1930
|
-
ttl
|
|
1931
|
-
});
|
|
1932
|
-
|
|
1933
|
-
// Note: DKIM records will be generated later when DKIM keys are available
|
|
1934
|
-
// They require the DKIMCreator which is part of the email server
|
|
1973
|
+
const requiredRecords = buildEmailDnsRecords({
|
|
1974
|
+
domain,
|
|
1975
|
+
hostname: this.options.emailConfig.hostname,
|
|
1976
|
+
mxPriority: domainConfig.dns?.internal?.mxPriority,
|
|
1977
|
+
}).filter((record) => !record.name.includes('._domainkey.'));
|
|
1978
|
+
|
|
1979
|
+
for (const record of requiredRecords) {
|
|
1980
|
+
records.push({
|
|
1981
|
+
name: record.name,
|
|
1982
|
+
type: record.type,
|
|
1983
|
+
value: record.value,
|
|
1984
|
+
ttl,
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1935
1987
|
}
|
|
1936
1988
|
|
|
1937
1989
|
logger.log('info', `Generated ${records.length} email DNS records for ${internalDnsDomains.length} internal-dns domains`);
|
|
@@ -1939,54 +1991,30 @@ export class DcRouter {
|
|
|
1939
1991
|
}
|
|
1940
1992
|
|
|
1941
1993
|
/**
|
|
1942
|
-
*
|
|
1943
|
-
* Reads all *.dkimrecord.json files from the DNS records directory
|
|
1994
|
+
* Generate DKIM DNS records for internal-dns domains from smartmta's selector-aware DKIM state.
|
|
1944
1995
|
*/
|
|
1945
1996
|
private async loadDkimRecords(): Promise<Array<{name: string; type: string; value: string; ttl?: number}>> {
|
|
1946
1997
|
const records: Array<{name: string; type: string; value: string; ttl?: number}> = [];
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
logger.log('debug', 'No DNS records directory found, skipping DKIM record loading');
|
|
1955
|
-
return records;
|
|
1998
|
+
if (!this.options.emailConfig?.domains || !this.emailServer?.dkimCreator) {
|
|
1999
|
+
return records;
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
for (const domainConfig of this.options.emailConfig.domains) {
|
|
2003
|
+
if (domainConfig.dnsMode !== 'internal-dns') {
|
|
2004
|
+
continue;
|
|
1956
2005
|
}
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
const fileContent = plugins.fs.readFileSync(filePath, 'utf8');
|
|
1969
|
-
const dkimRecord = JSON.parse(fileContent);
|
|
1970
|
-
|
|
1971
|
-
// Validate record structure
|
|
1972
|
-
if (dkimRecord.name && dkimRecord.type === 'TXT' && dkimRecord.value) {
|
|
1973
|
-
records.push({
|
|
1974
|
-
name: dkimRecord.name,
|
|
1975
|
-
type: 'TXT',
|
|
1976
|
-
value: dkimRecord.value,
|
|
1977
|
-
ttl: 3600 // Standard DKIM TTL
|
|
1978
|
-
});
|
|
1979
|
-
|
|
1980
|
-
logger.log('info', `Loaded DKIM record for ${dkimRecord.name}`);
|
|
1981
|
-
} else {
|
|
1982
|
-
logger.log('warn', `Invalid DKIM record structure in ${file}`);
|
|
1983
|
-
}
|
|
1984
|
-
} catch (error: unknown) {
|
|
1985
|
-
logger.log('error', `Failed to load DKIM record from ${file}: ${(error as Error).message}`);
|
|
1986
|
-
}
|
|
2006
|
+
const selector = domainConfig.dkim?.selector || 'default';
|
|
2007
|
+
try {
|
|
2008
|
+
const dkimRecord = await this.emailServer.dkimCreator.getDNSRecordForDomain(domainConfig.domain, selector);
|
|
2009
|
+
records.push({
|
|
2010
|
+
name: dkimRecord.name,
|
|
2011
|
+
type: 'TXT',
|
|
2012
|
+
value: dkimRecord.value,
|
|
2013
|
+
ttl: domainConfig.dns?.internal?.ttl || 3600,
|
|
2014
|
+
});
|
|
2015
|
+
} catch (error: unknown) {
|
|
2016
|
+
logger.log('error', `Failed to generate DKIM record for ${domainConfig.domain}: ${(error as Error).message}`);
|
|
1987
2017
|
}
|
|
1988
|
-
} catch (error: unknown) {
|
|
1989
|
-
logger.log('error', `Failed to load DKIM records: ${(error as Error).message}`);
|
|
1990
2018
|
}
|
|
1991
2019
|
|
|
1992
2020
|
return records;
|
|
@@ -2013,12 +2041,17 @@ export class DcRouter {
|
|
|
2013
2041
|
// Ensure necessary directories exist
|
|
2014
2042
|
paths.ensureDataDirectories(this.resolvedPaths);
|
|
2015
2043
|
|
|
2016
|
-
// Generate DKIM keys for each email domain
|
|
2044
|
+
// Generate DKIM keys for each internal-dns email domain using the configured selector.
|
|
2017
2045
|
for (const domainConfig of this.options.emailConfig.domains) {
|
|
2046
|
+
if (domainConfig.dnsMode !== 'internal-dns') {
|
|
2047
|
+
continue;
|
|
2048
|
+
}
|
|
2018
2049
|
try {
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2050
|
+
await dkimCreator.handleDKIMKeysForSelector(
|
|
2051
|
+
domainConfig.domain,
|
|
2052
|
+
domainConfig.dkim?.selector || 'default',
|
|
2053
|
+
domainConfig.dkim?.keySize || 2048,
|
|
2054
|
+
);
|
|
2022
2055
|
logger.log('info', `DKIM keys initialized for ${domainConfig.domain}`);
|
|
2023
2056
|
} catch (error: unknown) {
|
|
2024
2057
|
logger.log('error', `Failed to initialize DKIM for ${domainConfig.domain}: ${(error as Error).message}`);
|
|
@@ -2148,6 +2181,25 @@ export class DcRouter {
|
|
|
2148
2181
|
}
|
|
2149
2182
|
}
|
|
2150
2183
|
}
|
|
2184
|
+
|
|
2185
|
+
private addEmailEventSubscription(
|
|
2186
|
+
emitter: {
|
|
2187
|
+
on(eventName: string, listener: (...args: any[]) => void): void;
|
|
2188
|
+
off(eventName: string, listener: (...args: any[]) => void): void;
|
|
2189
|
+
},
|
|
2190
|
+
eventName: string,
|
|
2191
|
+
listener: (...args: any[]) => void,
|
|
2192
|
+
): void {
|
|
2193
|
+
emitter.on(eventName, listener);
|
|
2194
|
+
this.emailEventSubscriptions.push({ emitter, eventName, listener });
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
private clearEmailEventSubscriptions(): void {
|
|
2198
|
+
for (const subscription of this.emailEventSubscriptions) {
|
|
2199
|
+
subscription.emitter.off(subscription.eventName, subscription.listener);
|
|
2200
|
+
}
|
|
2201
|
+
this.emailEventSubscriptions = [];
|
|
2202
|
+
}
|
|
2151
2203
|
|
|
2152
2204
|
/**
|
|
2153
2205
|
* Detect the server's public IP address
|