@serve.zone/dcrouter 15.0.1 → 15.0.3

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 (117) hide show
  1. package/deno.json +1 -1
  2. package/dist_serve/bundle.js +768 -768
  3. package/dist_ts/00_commitinfo_data.js +1 -1
  4. package/dist_ts/acme/classes.smartacme-lifecycle.d.ts +25 -0
  5. package/dist_ts/acme/classes.smartacme-lifecycle.js +144 -0
  6. package/dist_ts/acme/index.d.ts +1 -0
  7. package/dist_ts/acme/index.js +2 -1
  8. package/dist_ts/classes.dcrouter.d.ts +21 -139
  9. package/dist_ts/classes.dcrouter.js +71 -1585
  10. package/dist_ts/dns/classes.dns-server-runtime.d.ts +37 -0
  11. package/dist_ts/dns/classes.dns-server-runtime.js +449 -0
  12. package/dist_ts/dns/index.d.ts +1 -0
  13. package/dist_ts/dns/index.js +2 -1
  14. package/dist_ts/email/classes.accepted-email-spool.d.ts +55 -0
  15. package/dist_ts/email/classes.accepted-email-spool.js +345 -0
  16. package/dist_ts/email/classes.email-route-builder.d.ts +28 -0
  17. package/dist_ts/email/classes.email-route-builder.js +260 -0
  18. package/dist_ts/email/index.d.ts +2 -0
  19. package/dist_ts/email/index.js +3 -1
  20. package/dist_ts/opsserver/handlers/gatewayclient.handler.js +10 -8
  21. package/dist_ts/remoteingress/classes.hub-lifecycle.d.ts +27 -0
  22. package/dist_ts/remoteingress/classes.hub-lifecycle.js +241 -0
  23. package/dist_ts/remoteingress/classes.remoteingress-manager.d.ts +1 -2
  24. package/dist_ts/remoteingress/index.d.ts +1 -0
  25. package/dist_ts/remoteingress/index.js +2 -1
  26. package/dist_ts/security/classes.route-policy-augmenter.d.ts +22 -0
  27. package/dist_ts/security/classes.route-policy-augmenter.js +120 -0
  28. package/dist_ts/security/index.d.ts +1 -0
  29. package/dist_ts/security/index.js +2 -1
  30. package/dist_ts/vpn/classes.vpn-access-resolver.d.ts +34 -0
  31. package/dist_ts/vpn/classes.vpn-access-resolver.js +101 -0
  32. package/dist_ts/vpn/index.d.ts +1 -0
  33. package/dist_ts/vpn/index.js +2 -1
  34. package/dist_ts_migrations/index.js +92 -9
  35. package/dist_ts_web/00_commitinfo_data.js +1 -1
  36. package/dist_ts_web/appstate/acme.d.ts +17 -0
  37. package/dist_ts_web/appstate/acme.js +64 -0
  38. package/dist_ts_web/appstate/certificates.d.ts +37 -0
  39. package/dist_ts_web/appstate/certificates.js +107 -0
  40. package/dist_ts_web/appstate/config.d.ts +9 -0
  41. package/dist_ts_web/appstate/config.js +35 -0
  42. package/dist_ts_web/appstate/domains.d.ts +80 -0
  43. package/dist_ts_web/appstate/domains.js +324 -0
  44. package/dist_ts_web/appstate/email-domains.d.ts +25 -0
  45. package/dist_ts_web/appstate/email-domains.js +104 -0
  46. package/dist_ts_web/appstate/email-ops.d.ts +10 -0
  47. package/dist_ts_web/appstate/email-ops.js +40 -0
  48. package/dist_ts_web/appstate/login.d.ts +30 -0
  49. package/dist_ts_web/appstate/login.js +83 -0
  50. package/dist_ts_web/appstate/logs.d.ts +16 -0
  51. package/dist_ts_web/appstate/logs.js +27 -0
  52. package/dist_ts_web/appstate/network.d.ts +50 -0
  53. package/dist_ts_web/appstate/network.js +122 -0
  54. package/dist_ts_web/appstate/profiles-targets.d.ts +45 -0
  55. package/dist_ts_web/appstate/profiles-targets.js +173 -0
  56. package/dist_ts_web/appstate/remoteingress.d.ts +47 -0
  57. package/dist_ts_web/appstate/remoteingress.js +204 -0
  58. package/dist_ts_web/appstate/routes.d.ts +76 -0
  59. package/dist_ts_web/appstate/routes.js +316 -0
  60. package/dist_ts_web/appstate/runtime.d.ts +1 -0
  61. package/dist_ts_web/appstate/runtime.js +276 -0
  62. package/dist_ts_web/appstate/security.d.ts +29 -0
  63. package/dist_ts_web/appstate/security.js +167 -0
  64. package/dist_ts_web/appstate/shared.d.ts +3 -0
  65. package/dist_ts_web/appstate/shared.js +13 -0
  66. package/dist_ts_web/appstate/stats.d.ts +15 -0
  67. package/dist_ts_web/appstate/stats.js +59 -0
  68. package/dist_ts_web/appstate/target-profiles.d.ts +37 -0
  69. package/dist_ts_web/appstate/target-profiles.js +118 -0
  70. package/dist_ts_web/appstate/ui.d.ts +11 -0
  71. package/dist_ts_web/appstate/ui.js +55 -0
  72. package/dist_ts_web/appstate/users.d.ts +27 -0
  73. package/dist_ts_web/appstate/users.js +85 -0
  74. package/dist_ts_web/appstate/vpn.d.ts +44 -0
  75. package/dist_ts_web/appstate/vpn.js +148 -0
  76. package/dist_ts_web/appstate.d.ts +20 -568
  77. package/dist_ts_web/appstate.js +24 -2418
  78. package/package.json +1 -1
  79. package/ts/00_commitinfo_data.ts +1 -1
  80. package/ts/acme/classes.smartacme-lifecycle.ts +155 -0
  81. package/ts/acme/index.ts +1 -0
  82. package/ts/classes.dcrouter.ts +118 -1919
  83. package/ts/dns/classes.dns-server-runtime.ts +525 -0
  84. package/ts/dns/index.ts +1 -0
  85. package/ts/email/classes.accepted-email-spool.ts +434 -0
  86. package/ts/email/classes.email-route-builder.ts +312 -0
  87. package/ts/email/index.ts +2 -0
  88. package/ts/opsserver/handlers/gatewayclient.handler.ts +9 -7
  89. package/ts/remoteingress/classes.hub-lifecycle.ts +278 -0
  90. package/ts/remoteingress/classes.remoteingress-manager.ts +1 -1
  91. package/ts/remoteingress/index.ts +1 -0
  92. package/ts/security/classes.route-policy-augmenter.ts +140 -0
  93. package/ts/security/index.ts +1 -0
  94. package/ts/vpn/classes.vpn-access-resolver.ts +126 -0
  95. package/ts/vpn/index.ts +1 -0
  96. package/ts_web/00_commitinfo_data.ts +1 -1
  97. package/ts_web/appstate/acme.ts +93 -0
  98. package/ts_web/appstate/certificates.ts +159 -0
  99. package/ts_web/appstate/config.ts +49 -0
  100. package/ts_web/appstate/domains.ts +429 -0
  101. package/ts_web/appstate/email-domains.ts +155 -0
  102. package/ts_web/appstate/email-ops.ts +57 -0
  103. package/ts_web/appstate/login.ts +128 -0
  104. package/ts_web/appstate/logs.ts +50 -0
  105. package/ts_web/appstate/network.ts +161 -0
  106. package/ts_web/appstate/profiles-targets.ts +240 -0
  107. package/ts_web/appstate/remoteingress.ts +300 -0
  108. package/ts_web/appstate/routes.ts +447 -0
  109. package/ts_web/appstate/runtime.ts +308 -0
  110. package/ts_web/appstate/security.ts +229 -0
  111. package/ts_web/appstate/shared.ts +15 -0
  112. package/ts_web/appstate/stats.ts +79 -0
  113. package/ts_web/appstate/target-profiles.ts +164 -0
  114. package/ts_web/appstate/ui.ts +75 -0
  115. package/ts_web/appstate/users.ts +133 -0
  116. package/ts_web/appstate/vpn.ts +234 -0
  117. package/ts_web/appstate.ts +24 -3403
@@ -0,0 +1,434 @@
1
+ import * as plugins from '../plugins.js';
2
+ import { logger } from '../logger.js';
3
+ import { CachedEmail } from '../db/index.js';
4
+ import type {
5
+ Email,
6
+ IExtendedSmtpSession,
7
+ IMessageAcceptanceContext,
8
+ IMessageAcceptanceDecision,
9
+ UnifiedEmailServer,
10
+ } from '@push.rocks/smartmta';
11
+ import type { DcRouter } from '../classes.dcrouter.js';
12
+
13
+ export const DCROUTER_CACHE_ID_HEADER = 'X-Dcrouter-Cached-Email-Id';
14
+ const ACCEPTED_EMAIL_SPOOL_INTERVAL_MS = 60_000;
15
+ const ACCEPTED_EMAIL_RETRY_DELAY_MS = 5 * 60_000;
16
+ const ACCEPTED_EMAIL_QUEUE_LEASE_MS = 30 * 60_000;
17
+ const ACCEPTED_EMAIL_SPOOL_BATCH_SIZE = 25;
18
+ const ACCEPTED_EMAIL_STOP_DRAIN_TIMEOUT_MS = 30_000;
19
+
20
+ export type TSmartMtaQueueItemLike = {
21
+ processingResult?: {
22
+ headers?: Record<string, string>;
23
+ email?: { headers?: Record<string, string> };
24
+ };
25
+ status?: 'pending' | 'processing' | 'queued' | 'delivered' | 'failed' | 'deferred';
26
+ attempts?: number;
27
+ nextAttempt?: Date;
28
+ lastError?: string;
29
+ };
30
+
31
+ type TStoredCachedEmailEnvelopeAddress = {
32
+ address: string;
33
+ args?: Record<string, string>;
34
+ };
35
+
36
+ type TStoredCachedEmailSession = {
37
+ id?: string;
38
+ clientHostname?: string;
39
+ remoteAddress?: string;
40
+ secure?: boolean;
41
+ authenticated?: boolean;
42
+ user?: IExtendedSmtpSession['user'];
43
+ envelope?: {
44
+ mailFrom?: TStoredCachedEmailEnvelopeAddress;
45
+ rcptTo?: TStoredCachedEmailEnvelopeAddress[];
46
+ };
47
+ };
48
+
49
+ type TStoredCachedEmailRouteData = {
50
+ session?: TStoredCachedEmailSession;
51
+ };
52
+
53
+ /**
54
+ * Accept-then-spool pipeline for inbound SMTP messages: persists accepted
55
+ * messages as CachedEmail docs, replays them through SmartMTA on an interval,
56
+ * and mirrors SmartMTA delivery-queue outcomes back onto the cached docs.
57
+ */
58
+ export class AcceptedEmailSpool {
59
+ private spoolTimer?: ReturnType<typeof setInterval> & { unref?: () => void };
60
+ private spoolRun?: Promise<void>;
61
+ private processing = false;
62
+ private stopping = false;
63
+ private queueUpdatePromises = new Set<Promise<void>>();
64
+
65
+ constructor(private dcRouterRef: DcRouter) {}
66
+
67
+ public async acceptMessage(
68
+ context: IMessageAcceptanceContext,
69
+ processAfterAccept = true,
70
+ ): Promise<IMessageAcceptanceDecision> {
71
+ if (!this.dcRouterRef.dcRouterDb?.isReady()) {
72
+ throw new Error('DcRouterDb is not available for email acceptance');
73
+ }
74
+ this.throwIfMessageAcceptanceAborted(context.abortSignal);
75
+
76
+ const rawMessage = context.rawMessage;
77
+ const session = context.session;
78
+ const envelope = session.envelope;
79
+ const envelopeRecipients = Array.isArray(envelope.rcptTo)
80
+ ? envelope.rcptTo.map((recipient) => recipient.address).filter(Boolean)
81
+ : [];
82
+ const email = context.email;
83
+ const headers = email.headers;
84
+
85
+ const cachedEmail = CachedEmail.createNew();
86
+ this.removeHeader(email.headers, DCROUTER_CACHE_ID_HEADER);
87
+ email.headers[DCROUTER_CACHE_ID_HEADER] = cachedEmail.id;
88
+ cachedEmail.messageId = headers['Message-ID'] || headers['message-id'] || cachedEmail.id;
89
+ cachedEmail.from = envelope.mailFrom?.address || email.from || '';
90
+ cachedEmail.to = envelopeRecipients.length > 0
91
+ ? envelopeRecipients
92
+ : Array.isArray(email.to) ? email.to : [];
93
+ cachedEmail.cc = Array.isArray(email.cc) ? email.cc : [];
94
+ cachedEmail.bcc = Array.isArray(email.bcc) ? email.bcc : [];
95
+ cachedEmail.subject = email.subject || '';
96
+ cachedEmail.rawContent = this.setDcRouterCacheIdHeader(rawMessage.toString('utf8'), cachedEmail.id);
97
+ cachedEmail.status = 'pending';
98
+ cachedEmail.nextAttempt = new Date();
99
+ cachedEmail.routeData = JSON.stringify({
100
+ acceptedAt: new Date().toISOString(),
101
+ session: {
102
+ id: session.id,
103
+ remoteAddress: session.remoteAddress,
104
+ clientHostname: session.clientHostname,
105
+ secure: !!session.secure,
106
+ authenticated: !!session.authenticated,
107
+ user: session.user,
108
+ envelope,
109
+ },
110
+ });
111
+ cachedEmail.updateSenderDomain();
112
+ await cachedEmail.save();
113
+ if (context.abortSignal?.aborted) {
114
+ cachedEmail.markFailed('Message acceptance aborted before SMTP success');
115
+ await cachedEmail.save();
116
+ throw new Error('Message acceptance aborted before SMTP success');
117
+ }
118
+
119
+ if (processAfterAccept) {
120
+ this.run();
121
+ } else {
122
+ cachedEmail.markDelivered();
123
+ await cachedEmail.save();
124
+ }
125
+
126
+ return {
127
+ accepted: true,
128
+ smtpCode: 250,
129
+ smtpMessage: '2.0.0 Message accepted for delivery',
130
+ continueProcessing: false,
131
+ };
132
+ }
133
+
134
+ /** Start the interval-driven spool processor and trigger an immediate run. */
135
+ public start(): void {
136
+ this.clearSpoolTimer();
137
+ this.stopping = false;
138
+ const runProcessor = () => {
139
+ this.run();
140
+ };
141
+ this.spoolTimer = setInterval(
142
+ runProcessor,
143
+ ACCEPTED_EMAIL_SPOOL_INTERVAL_MS,
144
+ ) as ReturnType<typeof setInterval> & { unref?: () => void };
145
+ this.spoolTimer.unref?.();
146
+ runProcessor();
147
+ }
148
+
149
+ /** Mark the spool as stopping and clear the interval without awaiting in-flight work. */
150
+ public beginStop(): void {
151
+ this.stopping = true;
152
+ this.clearSpoolTimer();
153
+ }
154
+
155
+ /** Stop the spool and wait (bounded) for an in-flight run to settle. */
156
+ public async stop(): Promise<void> {
157
+ this.beginStop();
158
+ const spoolRun = this.spoolRun;
159
+ if (spoolRun) {
160
+ const settled = await this.waitForPromiseToSettleWithTimeout(
161
+ spoolRun,
162
+ ACCEPTED_EMAIL_STOP_DRAIN_TIMEOUT_MS,
163
+ );
164
+ if (!settled) {
165
+ logger.log('warn', 'Timed out waiting for accepted email spool processing to stop');
166
+ }
167
+ }
168
+ }
169
+
170
+ /** Kick off a spool run unless one is already in flight. */
171
+ public run(): void {
172
+ if (this.spoolRun) {
173
+ return;
174
+ }
175
+ const run = this.processSpool().catch((error) => {
176
+ logger.log('warn', `Accepted email spool processing failed: ${(error as Error).message}`);
177
+ });
178
+ this.spoolRun = run;
179
+ void run.finally(() => {
180
+ if (this.spoolRun === run) {
181
+ this.spoolRun = undefined;
182
+ }
183
+ });
184
+ }
185
+
186
+ public trackQueueUpdate(
187
+ item: TSmartMtaQueueItemLike,
188
+ status: 'queued' | 'delivered' | 'failed',
189
+ failureMessage: string,
190
+ ): void {
191
+ const updatePromise = this.updateAcceptedEmailFromQueueItem(item, status).catch((error) => {
192
+ logger.log('warn', `${failureMessage}: ${(error as Error).message}`);
193
+ });
194
+ this.queueUpdatePromises.add(updatePromise);
195
+ void updatePromise.finally(() => {
196
+ this.queueUpdatePromises.delete(updatePromise);
197
+ });
198
+ }
199
+
200
+ public async drainQueueUpdates(): Promise<void> {
201
+ const queueUpdates = [...this.queueUpdatePromises];
202
+ if (queueUpdates.length === 0) {
203
+ return;
204
+ }
205
+ const settled = await this.waitForPromiseToSettleWithTimeout(
206
+ Promise.allSettled(queueUpdates).then(() => undefined),
207
+ ACCEPTED_EMAIL_STOP_DRAIN_TIMEOUT_MS,
208
+ );
209
+ if (!settled) {
210
+ for (const queueUpdate of queueUpdates) {
211
+ this.queueUpdatePromises.delete(queueUpdate);
212
+ }
213
+ logger.log('warn', `Timed out waiting for ${queueUpdates.length} accepted email queue update(s) to settle`);
214
+ }
215
+ }
216
+
217
+ /** Requeue emails left in 'queued' state by a previous process as pending. */
218
+ public async recoverQueuedEmails(): Promise<void> {
219
+ while (true) {
220
+ const queuedEmails = await CachedEmail.findQueuedForRecovery(ACCEPTED_EMAIL_SPOOL_BATCH_SIZE);
221
+ if (queuedEmails.length === 0) {
222
+ return;
223
+ }
224
+ for (const queuedEmail of queuedEmails) {
225
+ if (this.isCachedEmailTerminal(queuedEmail)) {
226
+ continue;
227
+ }
228
+ queuedEmail.status = 'pending';
229
+ queuedEmail.nextAttempt = new Date();
230
+ await queuedEmail.save();
231
+ }
232
+ if (queuedEmails.length < ACCEPTED_EMAIL_SPOOL_BATCH_SIZE) {
233
+ return;
234
+ }
235
+ }
236
+ }
237
+
238
+ private async processSpool(): Promise<void> {
239
+ const emailServer = this.dcRouterRef.emailServer;
240
+ if (this.processing || !emailServer || !this.dcRouterRef.dcRouterDb?.isReady()) {
241
+ return;
242
+ }
243
+ this.processing = true;
244
+ try {
245
+ const cachedEmails = await CachedEmail.findPendingForDelivery(ACCEPTED_EMAIL_SPOOL_BATCH_SIZE);
246
+ for (const cachedEmail of cachedEmails) {
247
+ if (this.stopping || this.dcRouterRef.emailServer !== emailServer) {
248
+ break;
249
+ }
250
+ const session = this.buildCachedEmailSession(cachedEmail);
251
+ const rawMessage = plugins.buffer.Buffer.from(cachedEmail.rawContent || '', 'utf8');
252
+ await this.processAcceptedCachedEmail(cachedEmail, rawMessage, session, emailServer);
253
+ }
254
+ } finally {
255
+ this.processing = false;
256
+ }
257
+ }
258
+
259
+ private async processAcceptedCachedEmail(
260
+ cachedEmail: CachedEmail,
261
+ emailData: Email | plugins.buffer.Buffer,
262
+ session: IExtendedSmtpSession,
263
+ emailServer: UnifiedEmailServer,
264
+ ): Promise<void> {
265
+ cachedEmail.status = 'processing';
266
+ cachedEmail.nextAttempt = new Date(Date.now() + ACCEPTED_EMAIL_QUEUE_LEASE_MS);
267
+ await cachedEmail.save();
268
+
269
+ try {
270
+ await emailServer.processEmailByMode(emailData, session);
271
+ if (session.matchedRoute?.action.type === 'forward') {
272
+ cachedEmail.markDelivered();
273
+ } else {
274
+ const currentCachedEmail = await CachedEmail.findById(cachedEmail.id) || cachedEmail;
275
+ if (this.isCachedEmailTerminal(currentCachedEmail)) {
276
+ return;
277
+ }
278
+ currentCachedEmail.status = 'queued';
279
+ currentCachedEmail.nextAttempt = new Date(Date.now() + ACCEPTED_EMAIL_QUEUE_LEASE_MS);
280
+ await currentCachedEmail.save();
281
+ return;
282
+ }
283
+ await cachedEmail.save();
284
+ } catch (error: unknown) {
285
+ cachedEmail.scheduleRetry(ACCEPTED_EMAIL_RETRY_DELAY_MS);
286
+ cachedEmail.lastError = (error as Error).message;
287
+ await cachedEmail.save();
288
+ logger.log('warn', `Accepted email ${cachedEmail.id} deferred after SmartMTA handoff failure: ${(error as Error).message}`);
289
+ }
290
+ }
291
+
292
+ private buildCachedEmailSession(cachedEmail: CachedEmail): IExtendedSmtpSession {
293
+ const routeData = this.parseCachedEmailRouteData(cachedEmail);
294
+ const storedSession = routeData.session || {};
295
+ const storedEnvelope = storedSession.envelope || {};
296
+ const storedRcptTo = Array.isArray(storedEnvelope.rcptTo) && storedEnvelope.rcptTo.length > 0
297
+ ? storedEnvelope.rcptTo
298
+ : cachedEmail.to.map((address) => ({ address, args: {} }));
299
+ const rcptTo = storedRcptTo
300
+ .filter((recipient) => !!recipient.address)
301
+ .map((recipient) => ({ address: recipient.address, args: recipient.args || {} }));
302
+ const mailFrom = storedEnvelope.mailFrom
303
+ ? { address: storedEnvelope.mailFrom.address, args: storedEnvelope.mailFrom.args || {} }
304
+ : { address: cachedEmail.from || '', args: {} };
305
+ const session = {
306
+ id: `${storedSession.id || cachedEmail.id}-replay-${Date.now()}`,
307
+ state: 'DATA' as unknown as IExtendedSmtpSession['state'],
308
+ clientHostname: storedSession.clientHostname || '',
309
+ mailFrom: mailFrom.address,
310
+ rcptTo: rcptTo.map((recipient) => recipient.address),
311
+ emailData: cachedEmail.rawContent || '',
312
+ useTLS: !!storedSession.secure,
313
+ connectionEnded: false,
314
+ remoteAddress: storedSession.remoteAddress || '127.0.0.1',
315
+ secure: !!storedSession.secure,
316
+ authenticated: !!storedSession.authenticated,
317
+ envelope: {
318
+ mailFrom,
319
+ rcptTo,
320
+ },
321
+ } as IExtendedSmtpSession;
322
+ if (storedSession.user) {
323
+ session.user = storedSession.user;
324
+ }
325
+ return session;
326
+ }
327
+
328
+ private parseCachedEmailRouteData(cachedEmail: CachedEmail): TStoredCachedEmailRouteData {
329
+ try {
330
+ return cachedEmail.routeData ? JSON.parse(cachedEmail.routeData) : {};
331
+ } catch {
332
+ return {};
333
+ }
334
+ }
335
+
336
+ private async updateAcceptedEmailFromQueueItem(
337
+ item: TSmartMtaQueueItemLike,
338
+ status: 'queued' | 'delivered' | 'failed',
339
+ ): Promise<void> {
340
+ const cachedEmailId = this.getCachedEmailIdFromQueueItem(item);
341
+ if (!cachedEmailId || !this.dcRouterRef.dcRouterDb?.isReady()) {
342
+ return;
343
+ }
344
+ const cachedEmail = await CachedEmail.findById(cachedEmailId);
345
+ if (!cachedEmail) {
346
+ return;
347
+ }
348
+ if (this.isCachedEmailTerminal(cachedEmail) && status !== 'delivered') {
349
+ return;
350
+ }
351
+ cachedEmail.attempts = Math.max(cachedEmail.attempts || 0, item.attempts || 0);
352
+ if (status === 'delivered') {
353
+ cachedEmail.markDelivered();
354
+ } else if (status === 'failed') {
355
+ cachedEmail.markFailed(item.lastError || 'SmartMTA delivery failed');
356
+ } else {
357
+ cachedEmail.status = 'queued';
358
+ cachedEmail.nextAttempt = new Date(Date.now() + ACCEPTED_EMAIL_QUEUE_LEASE_MS);
359
+ }
360
+ await cachedEmail.save();
361
+ }
362
+
363
+ private async waitForPromiseToSettleWithTimeout(
364
+ promise: Promise<unknown>,
365
+ timeoutMs: number,
366
+ ): Promise<boolean> {
367
+ let timeout: (ReturnType<typeof setTimeout> & { unref?: () => void }) | undefined;
368
+ return await new Promise<boolean>((resolve) => {
369
+ let settled = false;
370
+ const settle = (didSettle: boolean) => {
371
+ if (settled) {
372
+ return;
373
+ }
374
+ settled = true;
375
+ if (timeout) {
376
+ clearTimeout(timeout);
377
+ }
378
+ resolve(didSettle);
379
+ };
380
+ timeout = setTimeout(() => settle(false), timeoutMs) as ReturnType<typeof setTimeout> & { unref?: () => void };
381
+ timeout.unref?.();
382
+ promise.then(
383
+ () => settle(true),
384
+ () => settle(true),
385
+ );
386
+ });
387
+ }
388
+
389
+ private clearSpoolTimer(): void {
390
+ if (this.spoolTimer) {
391
+ clearInterval(this.spoolTimer);
392
+ this.spoolTimer = undefined;
393
+ }
394
+ }
395
+
396
+ private setDcRouterCacheIdHeader(rawContent: string, cachedEmailId: string): string {
397
+ const headerRegex = new RegExp(`^${DCROUTER_CACHE_ID_HEADER}:.*(?:\r?\n[\t ].*)*\r?\n?`, 'gim');
398
+ const sanitizedContent = rawContent.replace(headerRegex, '');
399
+ return `${DCROUTER_CACHE_ID_HEADER}: ${cachedEmailId}\r\n${sanitizedContent}`;
400
+ }
401
+
402
+ private isCachedEmailTerminal(cachedEmail: CachedEmail): boolean {
403
+ return cachedEmail.status === 'delivered' || cachedEmail.status === 'failed';
404
+ }
405
+
406
+ private getCachedEmailIdFromQueueItem(item: TSmartMtaQueueItemLike): string | undefined {
407
+ return this.getHeaderValue(item.processingResult?.headers, DCROUTER_CACHE_ID_HEADER)
408
+ || this.getHeaderValue(item.processingResult?.email?.headers, DCROUTER_CACHE_ID_HEADER);
409
+ }
410
+
411
+ private getHeaderValue(headers: Record<string, string> | undefined, headerName: string): string | undefined {
412
+ if (!headers) {
413
+ return undefined;
414
+ }
415
+ const normalizedHeaderName = headerName.toLowerCase();
416
+ const matchingHeaderName = Object.keys(headers).find((key) => key.toLowerCase() === normalizedHeaderName);
417
+ return matchingHeaderName ? headers[matchingHeaderName] : undefined;
418
+ }
419
+
420
+ private removeHeader(headers: Record<string, string>, headerName: string): void {
421
+ const normalizedHeaderName = headerName.toLowerCase();
422
+ for (const key of Object.keys(headers)) {
423
+ if (key.toLowerCase() === normalizedHeaderName) {
424
+ delete headers[key];
425
+ }
426
+ }
427
+ }
428
+
429
+ private throwIfMessageAcceptanceAborted(abortSignal: AbortSignal | undefined): void {
430
+ if (abortSignal?.aborted) {
431
+ throw new Error('Message acceptance aborted before SMTP success');
432
+ }
433
+ }
434
+ }