@serve.zone/dcrouter 15.11.0 → 16.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.
Files changed (73) hide show
  1. package/deno.json +1 -1
  2. package/dist_serve/bundle.js +34 -34
  3. package/dist_ts/00_commitinfo_data.js +2 -2
  4. package/dist_ts/classes.dcrouter.d.ts +10 -3
  5. package/dist_ts/classes.dcrouter.js +39 -43
  6. package/dist_ts/db/documents/classes.cached.email.d.ts +32 -0
  7. package/dist_ts/db/documents/classes.cached.email.js +67 -3
  8. package/dist_ts/db/documents/classes.email-domain.doc.d.ts +2 -1
  9. package/dist_ts/db/documents/classes.email-domain.doc.js +8 -2
  10. package/dist_ts/db/documents/classes.email-server-settings.doc.js +2 -2
  11. package/dist_ts/db/documents/classes.remote-ingress-edge.doc.d.ts +3 -0
  12. package/dist_ts/db/documents/classes.remote-ingress-edge.doc.js +20 -2
  13. package/dist_ts/dns/classes.dns-server-runtime.js +3 -1
  14. package/dist_ts/email/classes.accepted-email-spool.d.ts +5 -1
  15. package/dist_ts/email/classes.accepted-email-spool.js +9 -1
  16. package/dist_ts/email/classes.email-domain.manager.d.ts +17 -1
  17. package/dist_ts/email/classes.email-domain.manager.js +100 -17
  18. package/dist_ts/email/classes.email-route-builder.js +5 -2
  19. package/dist_ts/email/classes.email-settings.manager.js +3 -3
  20. package/dist_ts/email/classes.mail-dns-sync.d.ts +27 -0
  21. package/dist_ts/email/classes.mail-dns-sync.js +196 -0
  22. package/dist_ts/email/classes.mail-egress-coordinator.d.ts +37 -0
  23. package/dist_ts/email/classes.mail-egress-coordinator.js +113 -0
  24. package/dist_ts/email/index.d.ts +2 -0
  25. package/dist_ts/email/index.js +3 -1
  26. package/dist_ts/opsserver/handlers/config.handler.js +4 -2
  27. package/dist_ts/opsserver/handlers/email-domain.handler.js +3 -1
  28. package/dist_ts/opsserver/handlers/email-ops.handler.d.ts +20 -3
  29. package/dist_ts/opsserver/handlers/email-ops.handler.js +141 -19
  30. package/dist_ts/opsserver/handlers/email-settings.handler.js +11 -6
  31. package/dist_ts/opsserver/handlers/remoteingress.handler.js +16 -7
  32. package/dist_ts/remoteingress/classes.remoteingress-manager.d.ts +33 -1
  33. package/dist_ts/remoteingress/classes.remoteingress-manager.js +94 -2
  34. package/dist_ts/remoteingress/classes.tunnel-manager.d.ts +18 -0
  35. package/dist_ts/remoteingress/classes.tunnel-manager.js +114 -12
  36. package/dist_ts/remoteingress/edge-derivation-routes.d.ts +10 -0
  37. package/dist_ts/remoteingress/edge-derivation-routes.js +28 -0
  38. package/dist_ts/remoteingress/index.d.ts +1 -0
  39. package/dist_ts/remoteingress/index.js +2 -1
  40. package/dist_ts_interfaces/data/email-domain.d.ts +11 -1
  41. package/dist_ts_interfaces/data/email-settings.d.ts +11 -0
  42. package/dist_ts_interfaces/data/remoteingress.d.ts +10 -0
  43. package/dist_ts_interfaces/data/remoteingress.js +5 -2
  44. package/dist_ts_interfaces/requests/email-domains.d.ts +5 -1
  45. package/dist_ts_interfaces/requests/email-ops.d.ts +4 -0
  46. package/dist_ts_interfaces/requests/remoteingress.d.ts +11 -0
  47. package/dist_ts_migrations/index.js +91 -9
  48. package/dist_ts_web/00_commitinfo_data.js +2 -2
  49. package/package.json +4 -4
  50. package/ts/00_commitinfo_data.ts +1 -1
  51. package/ts/classes.dcrouter.ts +41 -48
  52. package/ts/db/documents/classes.cached.email.ts +72 -1
  53. package/ts/db/documents/classes.email-domain.doc.ts +4 -0
  54. package/ts/db/documents/classes.email-server-settings.doc.ts +1 -1
  55. package/ts/db/documents/classes.remote-ingress-edge.doc.ts +9 -0
  56. package/ts/dns/classes.dns-server-runtime.ts +2 -0
  57. package/ts/email/classes.accepted-email-spool.ts +12 -1
  58. package/ts/email/classes.email-domain.manager.ts +118 -17
  59. package/ts/email/classes.email-route-builder.ts +4 -1
  60. package/ts/email/classes.email-settings.manager.ts +2 -2
  61. package/ts/email/classes.mail-dns-sync.ts +218 -0
  62. package/ts/email/classes.mail-egress-coordinator.ts +125 -0
  63. package/ts/email/index.ts +2 -0
  64. package/ts/opsserver/handlers/config.handler.ts +3 -1
  65. package/ts/opsserver/handlers/email-domain.handler.ts +2 -0
  66. package/ts/opsserver/handlers/email-ops.handler.ts +156 -18
  67. package/ts/opsserver/handlers/email-settings.handler.ts +10 -5
  68. package/ts/opsserver/handlers/remoteingress.handler.ts +15 -5
  69. package/ts/remoteingress/classes.remoteingress-manager.ts +111 -0
  70. package/ts/remoteingress/classes.tunnel-manager.ts +125 -10
  71. package/ts/remoteingress/edge-derivation-routes.ts +29 -0
  72. package/ts/remoteingress/index.ts +1 -0
  73. package/ts_web/00_commitinfo_data.ts +1 -1
@@ -14,8 +14,6 @@ import {
14
14
  type IExtendedSmtpSession,
15
15
  type IMessageAcceptanceContext,
16
16
  type IMessageAcceptanceDecision,
17
- type IOutboundConnectionProxy,
18
- type IOutboundConnectionProxyContext,
19
17
  } from '@push.rocks/smartmta';
20
18
  import { logger } from './logger.js';
21
19
  import { StorageBackedCertManager } from './classes.storage-cert-manager.js';
@@ -29,7 +27,7 @@ import { commitinfo } from './00_commitinfo_data.js';
29
27
  import { OpsServer } from './opsserver/index.js';
30
28
  import { MetricsManager } from './monitoring/index.js';
31
29
  import { RadiusServer, type IRadiusServerConfig } from './radius/index.js';
32
- import { RemoteIngressHubLifecycle, RemoteIngressManager, TunnelManager } from './remoteingress/index.js';
30
+ import { RemoteIngressHubLifecycle, RemoteIngressManager, TunnelManager, buildEdgeOnlyDerivationRoutes } from './remoteingress/index.js';
33
31
  import { VpnAccessResolver, VpnManager, type IVpnManagerConfig } from './vpn/index.js';
34
32
  import { RouteConfigManager, ApiTokenManager, GatewayClientManager, ReferenceResolver, DbSeeder, TargetProfileManager, buildHttpRedirectRuntimeRoutes } from './config/index.js';
35
33
  import type { TVpnClientAllowEntry } from './config/classes.route-config-manager.js';
@@ -39,9 +37,9 @@ import { DnsManager } from './dns/manager.dns.js';
39
37
  import { DnsServerRuntime } from './dns/classes.dns-server-runtime.js';
40
38
  import { AcmeConfigManager } from './acme/manager.acme-config.js';
41
39
  import { SmartAcmeLifecycle } from './acme/classes.smartacme-lifecycle.js';
42
- import { AcceptedEmailSpool, EmailDomainManager, EmailRouteBuilder, EmailSettingsManager, SmartMtaStorageManager, WorkAppMailManager, type TSmartMtaQueueItemLike } from './email/index.js';
40
+ import { AcceptedEmailSpool, EmailDomainManager, EmailRouteBuilder, EmailSettingsManager, MailDnsSync, MailEgressCoordinator, SmartMtaStorageManager, WorkAppMailManager, type TSmartMtaQueueItemLike } from './email/index.js';
43
41
  import type { IRoute } from '../ts_interfaces/data/route-management.js';
44
- import type { IEmailPortConfig, IEmailServerSettings, IEmailServerSettingsSeed, TEmailOutboundMode, TEmailServerSettingsUpdate } from '../ts_interfaces/data/email-settings.js';
42
+ import type { IEmailOutboundEgressStatus, IEmailPortConfig, IEmailServerSettings, IEmailServerSettingsSeed, TEmailOutboundMode, TEmailServerSettingsUpdate } from '../ts_interfaces/data/email-settings.js';
45
43
  import type { IDcRouterRouteConfig, IRemoteIngressHubSettings, IRemoteIngressPerformanceConfig, TRemoteIngressHubSettingsUpdate } from '../ts_interfaces/data/remoteingress.js';
46
44
  import type { ISecurityCompiledPolicy } from '../ts_interfaces/data/security-policy.js';
47
45
 
@@ -311,6 +309,8 @@ export class DcRouter {
311
309
  public emailDomainManager?: EmailDomainManager;
312
310
  public workAppMailManager: WorkAppMailManager;
313
311
  public acceptedEmailSpool: AcceptedEmailSpool;
312
+ public mailEgressCoordinator: MailEgressCoordinator;
313
+ public mailDnsSync: MailDnsSync;
314
314
  public securityPolicyManager?: SecurityPolicyManager;
315
315
 
316
316
  // Auto-discovered public IP (populated by generateAuthoritativeRecords)
@@ -372,6 +372,8 @@ export class DcRouter {
372
372
  );
373
373
  this.workAppMailManager = new WorkAppMailManager(this);
374
374
  this.acceptedEmailSpool = new AcceptedEmailSpool(this);
375
+ this.mailEgressCoordinator = new MailEgressCoordinator(this);
376
+ this.mailDnsSync = new MailDnsSync(this);
375
377
  this.smartAcmeLifecycle = new SmartAcmeLifecycle(this);
376
378
  this.vpnAccessResolver = new VpnAccessResolver(this);
377
379
  this.remoteIngressHubLifecycle = new RemoteIngressHubLifecycle(this);
@@ -521,7 +523,14 @@ export class DcRouter {
521
523
  .dependsOn('DcRouterDb')
522
524
  .withStart(async () => {
523
525
  this.emailSettingsManager = new EmailSettingsManager(this.options);
524
- await this.emailSettingsManager.start();
526
+ try {
527
+ await this.emailSettingsManager.start();
528
+ } catch (error: unknown) {
529
+ // Loud failure: without this manager, outbound mail defers (strict mode
530
+ // resolution) — this log is the operator's pointer to the root cause.
531
+ logger.log('error', `EmailSettingsManager failed to start — DB-backed email settings unavailable, outbound mail will defer: ${(error as Error).message}`);
532
+ throw error;
533
+ }
525
534
  })
526
535
  .withStop(async () => {
527
536
  if (this.emailSettingsManager) {
@@ -566,6 +575,8 @@ export class DcRouter {
566
575
  .dependsOn('DcRouterDb')
567
576
  .withStart(async () => {
568
577
  this.remoteIngressManager = new RemoteIngressManager();
578
+ this.remoteIngressManager.setHubPublicIps([this.options.publicIp]);
579
+ this.remoteIngressManager.setOnEdgesChanged((reason) => this.mailDnsSync.requestSync(reason));
569
580
  await this.remoteIngressManager.initialize();
570
581
  })
571
582
  .withStop(async () => {
@@ -659,7 +670,10 @@ export class DcRouter {
659
670
  // then push updated derived ports to the Rust hub binary
660
671
  async (routes) => {
661
672
  try {
662
- await this.remoteIngressHubLifecycle.updateRoutes(routes as IDcRouterRouteConfig[]);
673
+ await this.remoteIngressHubLifecycle.updateRoutes([
674
+ ...(routes as IDcRouterRouteConfig[]),
675
+ ...this.getEdgeOnlyDerivationRoutes(),
676
+ ]);
663
677
  } catch (err: unknown) {
664
678
  logger.log('error', `Failed to sync Remote Ingress allowed edges: ${(err as Error).message}`);
665
679
  }
@@ -701,6 +715,10 @@ export class DcRouter {
701
715
  if (this.options.dbConfig?.enabled !== false) {
702
716
  const emailServiceDeps = ['SmartProxy', 'MetricsManager'];
703
717
  emailServiceDeps.push('EmailDomainManager');
718
+ // Explicit dependency: the outbound mode must come from the DB-backed settings;
719
+ // without it, deliveries defer via the coordinator's strict mode resolution
720
+ // instead of going direct.
721
+ emailServiceDeps.push('EmailSettingsManager');
704
722
  this.serviceManager.addService(
705
723
  new plugins.taskbuffer.Service('EmailServer')
706
724
  .optional()
@@ -1599,9 +1617,6 @@ export class DcRouter {
1599
1617
  // Create config with mapped ports
1600
1618
  const baseEmailConfig = this.options.emailConfig;
1601
1619
  const configuredMessageDataHook = baseEmailConfig.hooks?.onMessageData;
1602
- const outboundMode = this.emailSettingsManager?.getOutboundMode()
1603
- || this.options.emailOutboundMode
1604
- || 'direct';
1605
1620
  const mappedEmailPorts = baseEmailConfig.ports.map(port => portMapping[port] || port + 10000);
1606
1621
  const configuredSecurePort = baseEmailConfig.smtp?.securePort;
1607
1622
  const mappedSecurePort = configuredSecurePort === undefined
@@ -1619,9 +1634,7 @@ export class DcRouter {
1619
1634
  },
1620
1635
  outbound: {
1621
1636
  ...baseEmailConfig.outbound,
1622
- ...(outboundMode === 'remoteIngress'
1623
- ? { connectionProxyProvider: async (context) => this.createRemoteIngressEmailConnectionProxy(context) }
1624
- : {}),
1637
+ connectionProxyProvider: (context) => this.mailEgressCoordinator.provideConnectionProxy(context),
1625
1638
  },
1626
1639
  smtp: {
1627
1640
  ...baseEmailConfig.smtp,
@@ -1770,44 +1783,19 @@ export class DcRouter {
1770
1783
  }
1771
1784
 
1772
1785
  logger.log('info', `Email server started on ports: ${emailConfig.ports.join(', ')}`);
1786
+
1787
+ this.mailEgressCoordinator.logEgressPreflight();
1788
+ this.mailDnsSync.requestSync('email server start');
1773
1789
  }
1774
1790
 
1775
- private async createRemoteIngressEmailConnectionProxy(
1776
- context: IOutboundConnectionProxyContext,
1777
- ): Promise<IOutboundConnectionProxy> {
1778
- const tunnelManager = this.tunnelManager;
1779
- if (!tunnelManager) {
1780
- throw new Error('RemoteIngress outbound SMTP egress is enabled, but the hub is not running');
1781
- }
1782
-
1783
- const connectionTimeoutMs = this.options.emailConfig?.outbound?.connectionTimeout ?? 30_000;
1784
- const proxy = await tunnelManager.startEgressTcpProxy({
1785
- logicalHost: context.host,
1786
- port: context.port,
1787
- serverFirst: !context.secure,
1788
- connectTimeoutMs: connectionTimeoutMs,
1789
- acceptTimeoutMs: connectionTimeoutMs,
1790
- });
1791
- let closed = false;
1792
- return {
1793
- connectHost: proxy.listenHost,
1794
- connectPort: proxy.listenPort,
1795
- poolKey: `remoteIngress:${proxy.edgeId}:${context.host}:${context.port}`,
1796
- close: async () => {
1797
- if (closed) {
1798
- return;
1799
- }
1800
- closed = true;
1801
- try {
1802
- await tunnelManager.stopEgressTcpProxy(proxy.proxyId);
1803
- } catch (err: unknown) {
1804
- logger.log('warn', `Failed to stop RemoteIngress egress proxy ${proxy.proxyId}: ${(err as Error).message}`);
1805
- }
1806
- },
1807
- };
1791
+ /**
1792
+ * Readiness of the RemoteIngress outbound mail egress path (mail-tagged edges).
1793
+ */
1794
+ public getOutboundEgressStatus(): IEmailOutboundEgressStatus {
1795
+ return this.mailEgressCoordinator.getOutboundEgressStatus();
1808
1796
  }
1809
1797
 
1810
-
1798
+
1811
1799
  /**
1812
1800
  * Update the unified email configuration
1813
1801
  * @param config New email configuration
@@ -2017,7 +2005,12 @@ export class DcRouter {
2017
2005
 
2018
2006
  /** Bootstrap routes the RemoteIngress hub uses to derive edge ports before the DB route set is applied. */
2019
2007
  public getRemoteIngressBootstrapRoutes(): plugins.smartproxy.IRouteConfig[] {
2020
- return [...this.seedConfigRoutes, ...this.seedEmailRoutes, ...this.runtimeDnsRoutes];
2008
+ return [...this.seedConfigRoutes, ...this.seedEmailRoutes, ...this.runtimeDnsRoutes, ...this.getEdgeOnlyDerivationRoutes()];
2009
+ }
2010
+
2011
+ /** Edge-derivation-only routes (see buildEdgeOnlyDerivationRoutes) — never passed to SmartProxy. */
2012
+ public getEdgeOnlyDerivationRoutes(): IDcRouterRouteConfig[] {
2013
+ return buildEdgeOnlyDerivationRoutes(this.options);
2021
2014
  }
2022
2015
 
2023
2016
  /**
@@ -8,6 +8,12 @@ const TTL = plugins.smartdata.smartdataTtlValues;
8
8
  */
9
9
  export type TCachedEmailStatus = 'pending' | 'processing' | 'queued' | 'delivered' | 'failed' | 'deferred';
10
10
 
11
+ /**
12
+ * Direction of an accepted email: 'inbound' = received from a remote peer,
13
+ * 'outbound' = locally submitted for delivery.
14
+ */
15
+ export type TCachedEmailDirection = 'inbound' | 'outbound';
16
+
11
17
  /**
12
18
  * Helper to get the smartdata database instance
13
19
  */
@@ -112,6 +118,24 @@ export class CachedEmail extends plugins.smartdata.SmartdataCachedDocument<Cache
112
118
  @plugins.smartdata.svDb()
113
119
  public senderDomain!: string;
114
120
 
121
+ /**
122
+ * Direction of the accepted message (inbound receipt vs local submission)
123
+ */
124
+ @plugins.smartdata.svDb()
125
+ public direction: TCachedEmailDirection = 'outbound';
126
+
127
+ /**
128
+ * Lowercased recipient domains (for querying/filtering)
129
+ */
130
+ @plugins.smartdata.svDb()
131
+ public recipientDomains: string[] = [];
132
+
133
+ /**
134
+ * Epoch ms when the message was accepted (for newest-first listings)
135
+ */
136
+ @plugins.smartdata.svDb()
137
+ public acceptedAt: number = 0;
138
+
115
139
  /**
116
140
  * Priority level (higher = more important)
117
141
  */
@@ -183,7 +207,11 @@ export class CachedEmail extends plugins.smartdata.SmartdataCachedDocument<Cache
183
207
  }, limit);
184
208
  }
185
209
 
186
- private static async findLimited(filter: Record<string, any>, limit: number): Promise<CachedEmail[]> {
210
+ private static async findLimited(
211
+ filter: Record<string, any>,
212
+ limit: number,
213
+ sort?: Record<string, 1 | -1>,
214
+ ): Promise<CachedEmail[]> {
187
215
  const safeLimit = Number.isFinite(limit) ? Math.max(0, Math.floor(limit)) : 25;
188
216
  if (safeLimit === 0) {
189
217
  return [];
@@ -195,6 +223,9 @@ export class CachedEmail extends plugins.smartdata.SmartdataCachedDocument<Cache
195
223
  filter,
196
224
  CachedEmail as unknown as typeof plugins.smartdata.SmartDataDbDoc,
197
225
  );
226
+ if (sort) {
227
+ cursor.mongodbCursor.sort(sort);
228
+ }
198
229
  cursor.mongodbCursor.limit(safeLimit);
199
230
  try {
200
231
  return await cursor.toArray();
@@ -203,6 +234,32 @@ export class CachedEmail extends plugins.smartdata.SmartdataCachedDocument<Cache
203
234
  }
204
235
  }
205
236
 
237
+ /**
238
+ * Find the most recently accepted emails, newest first.
239
+ */
240
+ public static async findRecent(
241
+ limit = 200,
242
+ filter: { direction?: TCachedEmailDirection; recipientDomain?: string } = {},
243
+ ): Promise<CachedEmail[]> {
244
+ const query: Record<string, any> = {};
245
+ if (filter.direction) {
246
+ query.direction = filter.direction;
247
+ }
248
+ if (filter.recipientDomain) {
249
+ query.recipientDomains = filter.recipientDomain.toLowerCase();
250
+ }
251
+ return await CachedEmail.findLimited(query, limit, { acceptedAt: -1 });
252
+ }
253
+
254
+ /**
255
+ * Find emails addressed to a recipient domain
256
+ */
257
+ public static async findByRecipientDomain(domain: string): Promise<CachedEmail[]> {
258
+ return await CachedEmail.getInstances({
259
+ recipientDomains: { $all: [domain.toLowerCase()] },
260
+ });
261
+ }
262
+
206
263
  /**
207
264
  * Find emails by sender domain
208
265
  */
@@ -254,4 +311,18 @@ export class CachedEmail extends plugins.smartdata.SmartdataCachedDocument<Cache
254
311
  }
255
312
  }
256
313
  }
314
+
315
+ /**
316
+ * Derive recipient domains from the recipient address list
317
+ */
318
+ public updateRecipientDomains(): void {
319
+ const domains = new Set<string>();
320
+ for (const address of this.to || []) {
321
+ const match = address.match(/@([^>]+)>?$/);
322
+ if (match) {
323
+ domains.add(match[1].toLowerCase());
324
+ }
325
+ }
326
+ this.recipientDomains = [...domains];
327
+ }
257
328
  }
@@ -4,6 +4,7 @@ import type {
4
4
  IEmailDomainDkim,
5
5
  IEmailDomainRateLimits,
6
6
  IEmailDomainDnsStatus,
7
+ IEmailDomainRemoteIngress,
7
8
  } from '../../../ts_interfaces/data/email-domain.js';
8
9
 
9
10
  const getDb = () => DcRouterDb.getInstance().getDb();
@@ -29,6 +30,9 @@ export class EmailDomainDoc extends plugins.smartdata.SmartDataDbDoc<EmailDomain
29
30
  @plugins.smartdata.svDb()
30
31
  public rateLimits?: IEmailDomainRateLimits;
31
32
 
33
+ @plugins.smartdata.svDb()
34
+ public remoteIngress?: IEmailDomainRemoteIngress;
35
+
32
36
  @plugins.smartdata.svDb()
33
37
  public dnsStatus!: IEmailDomainDnsStatus;
34
38
 
@@ -18,7 +18,7 @@ export class EmailServerSettingsDoc extends plugins.smartdata.SmartDataDbDoc<Ema
18
18
  public emailConfig?: IUnifiedEmailServerOptions;
19
19
 
20
20
  @plugins.smartdata.svDb()
21
- public outboundMode: TEmailOutboundMode = 'direct';
21
+ public outboundMode: TEmailOutboundMode = 'remoteIngress';
22
22
 
23
23
  @plugins.smartdata.svDb()
24
24
  public emailPortConfig?: IEmailPortConfig;
@@ -37,6 +37,15 @@ export class RemoteIngressEdgeDoc extends plugins.smartdata.SmartDataDbDoc<Remot
37
37
  @plugins.smartdata.svDb()
38
38
  public tags!: string[];
39
39
 
40
+ @plugins.smartdata.svDb()
41
+ public publicIp?: string;
42
+
43
+ @plugins.smartdata.svDb()
44
+ public publicIpV6?: string;
45
+
46
+ @plugins.smartdata.svDb()
47
+ public mailHostname?: string;
48
+
40
49
  @plugins.smartdata.svDb()
41
50
  public createdAt!: number;
42
51
 
@@ -446,6 +446,7 @@ export class DnsServerRuntime {
446
446
  // Use explicitly configured public IP
447
447
  publicIp = options.publicIp;
448
448
  this.dcRouterRef.detectedPublicIp = publicIp;
449
+ this.dcRouterRef.remoteIngressManager?.setHubPublicIps([options.publicIp, publicIp]);
449
450
  logger.log('info', `Using configured public IP for nameserver A records: ${publicIp}`);
450
451
  } else {
451
452
  // Auto-discover public IP using smartnetwork
@@ -457,6 +458,7 @@ export class DnsServerRuntime {
457
458
  if (publicIps.v4) {
458
459
  publicIp = publicIps.v4;
459
460
  this.dcRouterRef.detectedPublicIp = publicIp;
461
+ this.dcRouterRef.remoteIngressManager?.setHubPublicIps([options.publicIp, publicIp]);
460
462
  logger.log('info', `Auto-discovered public IPv4: ${publicIp}`);
461
463
  } else {
462
464
  logger.log('warn', 'Could not auto-discover public IPv4 address');
@@ -1,6 +1,7 @@
1
1
  import * as plugins from '../plugins.js';
2
2
  import { logger } from '../logger.js';
3
3
  import { CachedEmail } from '../db/index.js';
4
+ import type { TCachedEmailDirection } from '../db/documents/classes.cached.email.js';
4
5
  import type {
5
6
  Email,
6
7
  IExtendedSmtpSession,
@@ -70,6 +71,8 @@ export interface IAcceptRawEmailOptions {
70
71
  messageId?: string;
71
72
  subject?: string;
72
73
  processAfterAccept?: boolean;
74
+ /** Explicit direction override for programmatic submitters. Defaults to the session-authenticated rule. */
75
+ direction?: TCachedEmailDirection;
73
76
  }
74
77
 
75
78
  /**
@@ -118,6 +121,8 @@ export class AcceptedEmailSpool {
118
121
  cachedEmail.rawContent = this.setDcRouterCacheIdHeader(rawMessage.toString('utf8'), cachedEmail.id);
119
122
  cachedEmail.status = 'pending';
120
123
  cachedEmail.nextAttempt = new Date();
124
+ cachedEmail.direction = session.authenticated ? 'outbound' : 'inbound';
125
+ cachedEmail.acceptedAt = Date.now();
121
126
  cachedEmail.routeData = JSON.stringify({
122
127
  acceptedAt: new Date().toISOString(),
123
128
  session: {
@@ -131,6 +136,7 @@ export class AcceptedEmailSpool {
131
136
  },
132
137
  });
133
138
  cachedEmail.updateSenderDomain();
139
+ cachedEmail.updateRecipientDomains();
134
140
  await cachedEmail.save();
135
141
  if (context.abortSignal?.aborted) {
136
142
  cachedEmail.markFailed('Message acceptance aborted before SMTP success');
@@ -171,6 +177,9 @@ export class AcceptedEmailSpool {
171
177
  cachedEmail.rawContent = this.setDcRouterCacheIdHeader(rawMessage, cachedEmail.id);
172
178
  cachedEmail.status = 'pending';
173
179
  cachedEmail.nextAttempt = new Date();
180
+ cachedEmail.direction = optionsArg.direction
181
+ ?? (optionsArg.session.authenticated ? 'outbound' : 'inbound');
182
+ cachedEmail.acceptedAt = Date.now();
174
183
  cachedEmail.routeData = JSON.stringify({
175
184
  acceptedAt: new Date().toISOString(),
176
185
  session: {
@@ -184,6 +193,7 @@ export class AcceptedEmailSpool {
184
193
  },
185
194
  });
186
195
  cachedEmail.updateSenderDomain();
196
+ cachedEmail.updateRecipientDomains();
187
197
  await cachedEmail.save();
188
198
 
189
199
  if (optionsArg.processAfterAccept !== false) {
@@ -506,7 +516,8 @@ export class AcceptedEmailSpool {
506
516
  return cachedEmail.status === 'delivered' || cachedEmail.status === 'failed';
507
517
  }
508
518
 
509
- private getCachedEmailIdFromQueueItem(item: TSmartMtaQueueItemLike): string | undefined {
519
+ /** The CachedEmail row a live queue item was spooled from (via the cache-id header). */
520
+ public getCachedEmailIdFromQueueItem(item: TSmartMtaQueueItemLike): string | undefined {
510
521
  return this.getHeaderValue(item.processingResult?.headers, DCROUTER_CACHE_ID_HEADER)
511
522
  || this.getHeaderValue(item.processingResult?.email?.headers, DCROUTER_CACHE_ID_HEADER);
512
523
  }