@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.
Files changed (43) hide show
  1. package/dist_serve/bundle.js +6 -5
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/classes.dcrouter.d.ts +9 -5
  4. package/dist_ts/classes.dcrouter.js +152 -120
  5. package/dist_ts/config/classes.route-config-manager.d.ts +13 -5
  6. package/dist_ts/config/classes.route-config-manager.js +76 -36
  7. package/dist_ts/db/documents/classes.route.doc.d.ts +2 -0
  8. package/dist_ts/db/documents/classes.route.doc.js +11 -2
  9. package/dist_ts/email/classes.email-domain.manager.d.ts +7 -0
  10. package/dist_ts/email/classes.email-domain.manager.js +118 -55
  11. package/dist_ts/email/classes.smartmta-storage-manager.d.ts +13 -0
  12. package/dist_ts/email/classes.smartmta-storage-manager.js +101 -0
  13. package/dist_ts/email/email-dns-records.d.ts +14 -0
  14. package/dist_ts/email/email-dns-records.js +34 -0
  15. package/dist_ts/email/index.d.ts +2 -0
  16. package/dist_ts/email/index.js +3 -1
  17. package/dist_ts/opsserver/handlers/email-ops.handler.js +6 -15
  18. package/dist_ts/opsserver/handlers/route-management.handler.js +5 -7
  19. package/dist_ts/opsserver/handlers/stats.handler.js +41 -7
  20. package/dist_ts_interfaces/data/route-management.d.ts +2 -0
  21. package/dist_ts_migrations/index.js +25 -1
  22. package/dist_ts_web/00_commitinfo_data.js +1 -1
  23. package/dist_ts_web/appstate.js +13 -4
  24. package/dist_ts_web/elements/network/ops-view-routes.d.ts +2 -0
  25. package/dist_ts_web/elements/network/ops-view-routes.js +44 -21
  26. package/package.json +2 -2
  27. package/readme.md +190 -1543
  28. package/ts/00_commitinfo_data.ts +1 -1
  29. package/ts/classes.dcrouter.ts +190 -138
  30. package/ts/config/classes.route-config-manager.ts +97 -42
  31. package/ts/db/documents/classes.route.doc.ts +7 -0
  32. package/ts/email/classes.email-domain.manager.ts +136 -51
  33. package/ts/email/classes.smartmta-storage-manager.ts +108 -0
  34. package/ts/email/email-dns-records.ts +53 -0
  35. package/ts/email/index.ts +2 -0
  36. package/ts/opsserver/handlers/email-ops.handler.ts +5 -19
  37. package/ts/opsserver/handlers/route-management.handler.ts +4 -6
  38. package/ts/opsserver/handlers/stats.handler.ts +43 -7
  39. package/ts_apiclient/readme.md +69 -195
  40. package/ts_web/00_commitinfo_data.ts +1 -1
  41. package/ts_web/appstate.ts +16 -4
  42. package/ts_web/elements/network/ops-view-routes.ts +47 -29
  43. package/ts_web/readme.md +41 -242
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '13.17.9',
6
+ version: '13.19.0',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -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/classes.email-domain.manager.js';
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
- // Compatibility shim for smartmta's DkimManager which calls dcRouter.storageManager.set()
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
- // Runtime-only DoH routes. These carry live socket handlers and must never be persisted.
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 = undefined;
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
- () => this.runtimeDnsRoutes,
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('SmartProxy')
631
+ .dependsOn(...emailServiceDeps)
617
632
  .withStart(async () => {
618
633
  await this.setupUnifiedEmailHandling();
619
634
  })
620
635
  .withStop(async () => {
621
636
  if (this.emailServer) {
622
- if ((this.emailServer as any).deliverySystem) {
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.runtimeDnsRoutes = this.generateDnsRoutes();
910
- logger.log('debug', `DNS routes for nameservers ${this.options.dnsNsDomains.join(', ')}`, { routes: JSON.stringify(this.runtimeDnsRoutes) });
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
- type: 'socket-handler' as any,
1356
- socketHandler: this.createDnsSocketHandler()
1357
- } as any
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
- hostname: 'localhost' // Listen on localhost for SmartProxy forwarding
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.on('error', (err: Error) => {
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.deliverySystem) {
1530
- this.emailServer.deliverySystem.on('deliveryStart', (item: any) => {
1531
- this.metricsManager!.trackEmailReceived(item?.from);
1532
- logger.log('info', `Email delivery started: ${item?.from} ${item?.to}`, { zone: 'email' });
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.deliverySystem.on('deliverySuccess', (item: any) => {
1535
- this.metricsManager!.trackEmailSent(item?.to);
1536
- logger.log('info', `Email delivered to ${item?.to}`, { zone: 'email' });
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.deliverySystem.on('deliveryFailed', (item: any, error: any) => {
1539
- this.metricsManager!.trackEmailFailed(item?.to, error?.message);
1540
- logger.log('warn', `Email delivery failed to ${item?.to}: ${error?.message}`, { zone: 'email' });
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
- if (this.metricsManager && this.emailServer) {
1544
- this.emailServer.on('bounceProcessed', () => {
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
- // Remove listeners before stopping to prevent leaks on config update cycles
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
- // Generate email DNS records
1787
- const emailDnsRecords = await this.generateEmailDnsRecords();
1788
-
1789
- // Initialize DKIM for all email domains
1790
- await this.initializeDkimForEmailDomains();
1791
-
1792
- // Load DKIM records from JSON files (they should now exist)
1793
- const dkimRecords = await this.loadDkimRecords();
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 mxPriority = domainConfig.dns?.internal?.mxPriority || 10;
1905
-
1906
- // MX record - points to the domain itself for email handling
1907
- records.push({
1908
- name: domain,
1909
- type: 'MX',
1910
- value: `${mxPriority} ${domain}`,
1911
- ttl
1912
- });
1913
-
1914
- // SPF record - using sensible defaults
1915
- const spfRecord = 'v=spf1 a mx ~all';
1916
- records.push({
1917
- name: domain,
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
- * Load DKIM records from JSON files
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
- try {
1949
- // Ensure paths are imported
1950
- const dnsDir = this.resolvedPaths.dnsRecordsDir;
1951
-
1952
- // Check if directory exists
1953
- if (!plugins.fs.existsSync(dnsDir)) {
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
- // Read all files in the directory
1959
- const files = plugins.fs.readdirSync(dnsDir);
1960
- const dkimFiles = files.filter(f => f.endsWith('.dkimrecord.json'));
1961
-
1962
- logger.log('info', `Found ${dkimFiles.length} DKIM record files`);
1963
-
1964
- // Load each DKIM record
1965
- for (const file of dkimFiles) {
1966
- try {
1967
- const filePath = plugins.path.join(dnsDir, file);
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
- // Generate DKIM keys for all domains, regardless of DNS mode
2020
- // This ensures keys are ready even if DNS mode changes later
2021
- await dkimCreator.handleDKIMKeysForDomain(domainConfig.domain);
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