@push.rocks/smartmta 6.4.1 → 6.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -30,6 +30,20 @@ interface IDnsRecords {
30
30
  ns?: string[];
31
31
  }
32
32
 
33
+ interface IDnsResolver {
34
+ setServers(servers: string[]): void;
35
+ resolveMx(domain: string): Promise<Array<{ exchange: string; priority: number }>>;
36
+ resolveTxt(domain: string): Promise<string[][]>;
37
+ resolveNs(domain: string): Promise<string[]>;
38
+ cancel(): void;
39
+ }
40
+
41
+ interface IDnsValidationRun {
42
+ id: number;
43
+ abortController: AbortController;
44
+ cancel: () => void;
45
+ }
46
+
33
47
  /**
34
48
  * Manages DNS configuration for email domains
35
49
  * Handles both validation and creation of DNS records
@@ -37,37 +51,54 @@ interface IDnsRecords {
37
51
  export class DnsManager {
38
52
  private dcRouter: IDcRouterLike;
39
53
  private storageManager: IStorageManagerLike;
54
+ private resolverFactory: () => IDnsResolver;
55
+ private activeResolvers = new Set<IDnsResolver>();
56
+ private activeValidation?: IDnsValidationRun;
57
+ private validationRunId = 0;
40
58
 
41
- constructor(dcRouter: IDcRouterLike) {
59
+ constructor(
60
+ dcRouter: IDcRouterLike,
61
+ resolverFactory: () => IDnsResolver = () => new plugins.dns.promises.Resolver()
62
+ ) {
42
63
  this.dcRouter = dcRouter;
43
64
  this.storageManager = dcRouter.storageManager;
65
+ this.resolverFactory = resolverFactory;
44
66
  }
45
67
 
46
68
  /**
47
69
  * Validate all domain configurations
48
70
  */
49
- async validateAllDomains(domainConfigs: IEmailDomainConfig[]): Promise<Map<string, IDnsValidationResult>> {
50
- const results = new Map<string, IDnsValidationResult>();
51
-
52
- for (const config of domainConfigs) {
53
- const result = await this.validateDomain(config);
54
- results.set(config.domain, result);
71
+ async validateAllDomains(
72
+ domainConfigs: IEmailDomainConfig[],
73
+ abortSignal?: AbortSignal
74
+ ): Promise<Map<string, IDnsValidationResult>> {
75
+ if (abortSignal?.aborted) {
76
+ return new Map();
55
77
  }
56
-
57
- return results;
78
+ const results = await Promise.all(domainConfigs.map(async (config) => {
79
+ return [config.domain, await this.validateDomain(config, abortSignal)] as const;
80
+ }));
81
+
82
+ if (abortSignal?.aborted) {
83
+ return new Map();
84
+ }
85
+ return new Map(results);
58
86
  }
59
87
 
60
88
  /**
61
89
  * Validate a single domain configuration
62
90
  */
63
- async validateDomain(config: IEmailDomainConfig): Promise<IDnsValidationResult> {
91
+ async validateDomain(
92
+ config: IEmailDomainConfig,
93
+ abortSignal?: AbortSignal
94
+ ): Promise<IDnsValidationResult> {
64
95
  switch (config.dnsMode) {
65
96
  case 'forward':
66
97
  return this.validateForwardMode(config);
67
98
  case 'internal-dns':
68
- return this.validateInternalDnsMode(config);
99
+ return this.validateInternalDnsMode(config, abortSignal);
69
100
  case 'external-dns':
70
- return this.validateExternalDnsMode(config);
101
+ return this.validateExternalDnsMode(config, abortSignal);
71
102
  default:
72
103
  return {
73
104
  valid: false,
@@ -105,7 +136,10 @@ export class DnsManager {
105
136
  /**
106
137
  * Validate internal DNS mode configuration
107
138
  */
108
- private async validateInternalDnsMode(config: IEmailDomainConfig): Promise<IDnsValidationResult> {
139
+ private async validateInternalDnsMode(
140
+ config: IEmailDomainConfig,
141
+ abortSignal?: AbortSignal
142
+ ): Promise<IDnsValidationResult> {
109
143
  const result: IDnsValidationResult = {
110
144
  valid: true,
111
145
  errors: [],
@@ -164,7 +198,10 @@ export class DnsManager {
164
198
 
165
199
  // Check NS delegation
166
200
  try {
167
- const nsRecords = await this.resolveNs(config.domain);
201
+ const nsRecords = await this.resolveNs(config.domain, abortSignal);
202
+ if (abortSignal?.aborted) {
203
+ return result;
204
+ }
168
205
  const delegatedNameservers = dnsNsDomains.filter(ns => nsRecords.includes(ns));
169
206
  const isDelegated = delegatedNameservers.length > 0;
170
207
 
@@ -192,6 +229,9 @@ export class DnsManager {
192
229
  );
193
230
  }
194
231
  } catch (error) {
232
+ if (abortSignal?.aborted) {
233
+ return result;
234
+ }
195
235
  result.warnings.push(
196
236
  `Could not verify NS delegation for ${config.domain}: ${error.message}`
197
237
  );
@@ -203,7 +243,10 @@ export class DnsManager {
203
243
  /**
204
244
  * Validate external DNS mode configuration
205
245
  */
206
- private async validateExternalDnsMode(config: IEmailDomainConfig): Promise<IDnsValidationResult> {
246
+ private async validateExternalDnsMode(
247
+ config: IEmailDomainConfig,
248
+ abortSignal?: AbortSignal
249
+ ): Promise<IDnsValidationResult> {
207
250
  const result: IDnsValidationResult = {
208
251
  valid: true,
209
252
  errors: [],
@@ -213,7 +256,10 @@ export class DnsManager {
213
256
 
214
257
  try {
215
258
  // Get current DNS records
216
- const records = await this.checkDnsRecords(config);
259
+ const records = await this.checkDnsRecords(config, abortSignal);
260
+ if (abortSignal?.aborted) {
261
+ return result;
262
+ }
217
263
  const requiredRecords = config.dns?.external?.requiredRecords || ['MX', 'SPF', 'DKIM', 'DMARC'];
218
264
 
219
265
  // Check MX record
@@ -233,22 +279,9 @@ export class DnsManager {
233
279
  // Check DKIM record
234
280
  if (requiredRecords.includes('DKIM') && !records.dkim) {
235
281
  const selector = config.dkim?.selector || 'default';
236
- const dkimPublicKey = await this.storageManager.get(`/email/dkim/${config.domain}/public.key`);
237
-
238
- if (dkimPublicKey) {
239
- const publicKeyBase64 = dkimPublicKey
240
- .replace(/-----BEGIN PUBLIC KEY-----/g, '')
241
- .replace(/-----END PUBLIC KEY-----/g, '')
242
- .replace(/\s/g, '');
243
-
244
- result.requiredChanges.push(
245
- `Add TXT record: ${selector}._domainkey.${config.domain} -> "v=DKIM1; k=rsa; p=${publicKeyBase64}"`
246
- );
247
- } else {
248
- result.warnings.push(
249
- `DKIM public key not found for ${config.domain}. It will be generated on first use.`
250
- );
251
- }
282
+ result.requiredChanges.push(
283
+ `Add TXT record: ${selector}._domainkey.${config.domain} -> "v=DKIM1; k=rsa; p=<generated-public-key>"`
284
+ );
252
285
  }
253
286
 
254
287
  // Check DMARC record
@@ -269,6 +302,9 @@ export class DnsManager {
269
302
  }
270
303
 
271
304
  } catch (error) {
305
+ if (abortSignal?.aborted) {
306
+ return result;
307
+ }
272
308
  result.errors.push(`DNS validation failed: ${error.message}`);
273
309
  result.valid = false;
274
310
  }
@@ -279,62 +315,80 @@ export class DnsManager {
279
315
  /**
280
316
  * Check DNS records for a domain
281
317
  */
282
- private async checkDnsRecords(config: IEmailDomainConfig): Promise<IDnsRecords> {
318
+ private async checkDnsRecords(
319
+ config: IEmailDomainConfig,
320
+ abortSignal?: AbortSignal
321
+ ): Promise<IDnsRecords> {
283
322
  const records: IDnsRecords = {};
284
323
  const baseDomain = this.getBaseDomain(config.domain);
285
324
  const selector = config.dkim?.selector || 'default';
286
325
 
287
326
  // Use custom DNS servers if specified
288
- const resolver = new plugins.dns.promises.Resolver();
327
+ const resolver = this.createTrackedResolver();
289
328
  if (config.dns?.external?.servers?.length) {
290
329
  resolver.setServers(config.dns.external.servers);
291
330
  }
292
-
293
- // Check MX records
294
- try {
295
- const mxRecords = await resolver.resolveMx(baseDomain);
296
- records.mx = mxRecords.map(mx => mx.exchange);
297
- } catch (error) {
298
- logger.log('debug', `No MX records found for ${baseDomain}`);
299
- }
300
-
301
- // Check SPF record
302
- try {
303
- const txtRecords = await resolver.resolveTxt(baseDomain);
304
- const spfRecord = txtRecords.find(records =>
305
- records.some(record => record.startsWith('v=spf1'))
306
- );
307
- if (spfRecord) {
308
- records.spf = spfRecord.join('');
309
- }
310
- } catch (error) {
311
- logger.log('debug', `No SPF record found for ${baseDomain}`);
312
- }
313
-
314
- // Check DKIM record
315
- try {
316
- const dkimRecords = await resolver.resolveTxt(`${selector}._domainkey.${config.domain}`);
317
- const dkimRecord = dkimRecords.find(records =>
318
- records.some(record => record.includes('v=DKIM1'))
319
- );
320
- if (dkimRecord) {
321
- records.dkim = dkimRecord.join('');
322
- }
323
- } catch (error) {
324
- logger.log('debug', `No DKIM record found for ${selector}._domainkey.${config.domain}`);
325
- }
326
-
327
- // Check DMARC record
331
+
328
332
  try {
329
- const dmarcRecords = await resolver.resolveTxt(`_dmarc.${baseDomain}`);
330
- const dmarcRecord = dmarcRecords.find(records =>
331
- records.some(record => record.startsWith('v=DMARC1'))
332
- );
333
- if (dmarcRecord) {
334
- records.dmarc = dmarcRecord.join('');
335
- }
336
- } catch (error) {
337
- logger.log('debug', `No DMARC record found for _dmarc.${baseDomain}`);
333
+ await Promise.all([
334
+ (async () => {
335
+ try {
336
+ const mxRecords = await resolver.resolveMx(baseDomain);
337
+ records.mx = mxRecords.map(mx => mx.exchange);
338
+ } catch {
339
+ if (!abortSignal?.aborted) {
340
+ logger.log('debug', `No MX records found for ${baseDomain}`);
341
+ }
342
+ }
343
+ })(),
344
+ (async () => {
345
+ try {
346
+ const txtRecords = await resolver.resolveTxt(baseDomain);
347
+ const spfRecord = txtRecords.find(txtParts =>
348
+ txtParts.some(record => record.startsWith('v=spf1'))
349
+ );
350
+ if (spfRecord) {
351
+ records.spf = spfRecord.join('');
352
+ }
353
+ } catch {
354
+ if (!abortSignal?.aborted) {
355
+ logger.log('debug', `No SPF record found for ${baseDomain}`);
356
+ }
357
+ }
358
+ })(),
359
+ (async () => {
360
+ try {
361
+ const dkimRecords = await resolver.resolveTxt(`${selector}._domainkey.${config.domain}`);
362
+ const dkimRecord = dkimRecords.find(txtParts =>
363
+ txtParts.some(record => record.includes('v=DKIM1'))
364
+ );
365
+ if (dkimRecord) {
366
+ records.dkim = dkimRecord.join('');
367
+ }
368
+ } catch {
369
+ if (!abortSignal?.aborted) {
370
+ logger.log('debug', `No DKIM record found for ${selector}._domainkey.${config.domain}`);
371
+ }
372
+ }
373
+ })(),
374
+ (async () => {
375
+ try {
376
+ const dmarcRecords = await resolver.resolveTxt(`_dmarc.${baseDomain}`);
377
+ const dmarcRecord = dmarcRecords.find(txtParts =>
378
+ txtParts.some(record => record.startsWith('v=DMARC1'))
379
+ );
380
+ if (dmarcRecord) {
381
+ records.dmarc = dmarcRecord.join('');
382
+ }
383
+ } catch {
384
+ if (!abortSignal?.aborted) {
385
+ logger.log('debug', `No DMARC record found for _dmarc.${baseDomain}`);
386
+ }
387
+ }
388
+ })(),
389
+ ]);
390
+ } finally {
391
+ this.activeResolvers.delete(resolver);
338
392
  }
339
393
 
340
394
  return records;
@@ -343,16 +397,37 @@ export class DnsManager {
343
397
  /**
344
398
  * Resolve NS records for a domain
345
399
  */
346
- private async resolveNs(domain: string): Promise<string[]> {
400
+ private async resolveNs(domain: string, abortSignal?: AbortSignal): Promise<string[]> {
401
+ const resolver = this.createTrackedResolver();
347
402
  try {
348
- const resolver = new plugins.dns.promises.Resolver();
349
403
  const nsRecords = await resolver.resolveNs(domain);
350
404
  return nsRecords;
351
405
  } catch (error) {
352
- logger.log('warn', `Failed to resolve NS records for ${domain}: ${error.message}`);
406
+ if (!abortSignal?.aborted) {
407
+ logger.log('warn', `Failed to resolve NS records for ${domain}: ${error.message}`);
408
+ }
353
409
  return [];
410
+ } finally {
411
+ this.activeResolvers.delete(resolver);
354
412
  }
355
413
  }
414
+
415
+ private createTrackedResolver(): IDnsResolver {
416
+ const resolver = this.resolverFactory();
417
+ this.activeResolvers.add(resolver);
418
+ return resolver;
419
+ }
420
+
421
+ private cancelActiveResolvers(): void {
422
+ for (const resolver of this.activeResolvers) {
423
+ try {
424
+ resolver.cancel();
425
+ } catch {
426
+ // Resolver may already have completed or been cancelled.
427
+ }
428
+ }
429
+ this.activeResolvers.clear();
430
+ }
356
431
 
357
432
  /**
358
433
  * Get base domain from email domain (e.g., mail.example.com -> example.com)
@@ -373,17 +448,10 @@ export class DnsManager {
373
448
  return parts.slice(-2).join('.');
374
449
  }
375
450
 
376
- /**
377
- * Ensure all DNS records are created for configured domains
378
- * This is the main entry point for DNS record management
379
- */
380
- async ensureDnsRecords(domainConfigs: IEmailDomainConfig[], dkimCreator?: any): Promise<void> {
381
- logger.log('info', `Ensuring DNS records for ${domainConfigs.length} domains`);
382
-
383
- // First, validate all domains
384
- const validationResults = await this.validateAllDomains(domainConfigs);
385
-
386
- // Then create records for internal-dns domains
451
+ /** Provision deterministic local DNS records without performing network lookups. */
452
+ async provisionLocalDnsRecords(domainConfigs: IEmailDomainConfig[], dkimCreator?: any): Promise<void> {
453
+ logger.log('info', `Provisioning local DNS records for ${domainConfigs.length} domains`);
454
+
387
455
  const internalDnsDomains = domainConfigs.filter(config => config.dnsMode === 'internal-dns');
388
456
  if (internalDnsDomains.length > 0) {
389
457
  await this.createInternalDnsRecords(internalDnsDomains);
@@ -393,8 +461,81 @@ export class DnsManager {
393
461
  await this.createDkimRecords(domainConfigs, dkimCreator);
394
462
  }
395
463
  }
396
-
397
- // Log validation results for external-dns domains
464
+ }
465
+
466
+ /** Validate public DNS state with a single bounded, cancellable validation run. */
467
+ async validateAndReportDnsRecords(
468
+ domainConfigs: IEmailDomainConfig[],
469
+ timeoutMs = 30_000
470
+ ): Promise<void> {
471
+ this.cancelValidation();
472
+
473
+ const runId = ++this.validationRunId;
474
+ const abortController = new AbortController();
475
+ let cancelRun!: () => void;
476
+ const cancellationPromise = new Promise<{ kind: 'cancelled' }>((resolve) => {
477
+ cancelRun = () => resolve({ kind: 'cancelled' });
478
+ });
479
+ this.activeValidation = { id: runId, abortController, cancel: cancelRun };
480
+
481
+ let timeout: NodeJS.Timeout | undefined;
482
+ const timeoutPromise = new Promise<{ kind: 'timeout' }>((resolve) => {
483
+ timeout = setTimeout(() => {
484
+ abortController.abort();
485
+ this.cancelActiveResolvers();
486
+ resolve({ kind: 'timeout' });
487
+ }, timeoutMs);
488
+ });
489
+ const validationPromise = this.validateAllDomains(
490
+ domainConfigs,
491
+ abortController.signal
492
+ ).then((results) => ({
493
+ kind: 'completed' as const,
494
+ results,
495
+ }));
496
+
497
+ try {
498
+ const outcome = await Promise.race([
499
+ validationPromise,
500
+ timeoutPromise,
501
+ cancellationPromise,
502
+ ]);
503
+
504
+ if (outcome.kind === 'cancelled') {
505
+ return;
506
+ }
507
+ if (outcome.kind === 'timeout') {
508
+ throw new Error(`DNS validation timed out after ${timeoutMs} ms`);
509
+ }
510
+ if (this.activeValidation?.id !== runId) {
511
+ return;
512
+ }
513
+
514
+ this.reportValidationResults(domainConfigs, outcome.results);
515
+ } finally {
516
+ if (timeout) {
517
+ clearTimeout(timeout);
518
+ }
519
+ if (this.activeValidation?.id === runId) {
520
+ this.activeValidation = undefined;
521
+ this.cancelActiveResolvers();
522
+ }
523
+ }
524
+ }
525
+
526
+ /** Cancel the current validation run and all DNS requests owned by it. */
527
+ cancelValidation(): void {
528
+ const activeValidation = this.activeValidation;
529
+ this.activeValidation = undefined;
530
+ activeValidation?.abortController.abort();
531
+ activeValidation?.cancel();
532
+ this.cancelActiveResolvers();
533
+ }
534
+
535
+ private reportValidationResults(
536
+ domainConfigs: IEmailDomainConfig[],
537
+ validationResults: Map<string, IDnsValidationResult>
538
+ ): void {
398
539
  for (const [domain, result] of validationResults) {
399
540
  const config = domainConfigs.find(c => c.domain === domain);
400
541
  if (config?.dnsMode === 'external-dns' && result.requiredChanges.length > 0) {
@@ -402,6 +543,15 @@ export class DnsManager {
402
543
  }
403
544
  }
404
545
  }
546
+
547
+ /**
548
+ * Ensure DNS records and validate public DNS state.
549
+ * Prefer the split lifecycle methods when validation must not block readiness.
550
+ */
551
+ async ensureDnsRecords(domainConfigs: IEmailDomainConfig[], dkimCreator?: any): Promise<void> {
552
+ await this.provisionLocalDnsRecords(domainConfigs, dkimCreator);
553
+ await this.validateAndReportDnsRecords(domainConfigs);
554
+ }
405
555
 
406
556
  /**
407
557
  * Create DNS records for internal-dns mode domains
@@ -254,6 +254,10 @@ export class UnifiedEmailServer extends EventEmitter {
254
254
  // Extracted subsystems
255
255
  private actionExecutor: EmailActionExecutor;
256
256
  private dkimManager: DkimManager;
257
+ private dnsManager: DnsManager;
258
+ private dnsValidationTask?: Promise<void>;
259
+ private dnsValidationGeneration = 0;
260
+ private localDnsProvisioned = false;
257
261
  private bridgeStateChangeHandler?: (event: { oldState: string; newState: string }) => void;
258
262
  private bridgeRcptToRequestHandler?: (data: IRcptToRequestEvent) => void;
259
263
  private bridgeEmailReceivedHandler?: (data: IEmailReceivedEvent) => void;
@@ -343,6 +347,7 @@ export class UnifiedEmailServer extends EventEmitter {
343
347
 
344
348
  // Initialize DKIM manager
345
349
  this.dkimManager = new DkimManager(this.dkimCreator, this.domainRegistry, dcRouter, this.rustBridge);
350
+ this.dnsManager = new DnsManager(this.dcRouter);
346
351
 
347
352
  // Initialize statistics
348
353
  this.stats = {
@@ -374,6 +379,22 @@ export class UnifiedEmailServer extends EventEmitter {
374
379
  return this.options.outbound?.hostname || this.options.hostname;
375
380
  }
376
381
 
382
+ /**
383
+ * Whether a hostname refers to this server's own SMTP identity.
384
+ * Used by MTA delivery to refuse self-addressed relaying: a domain whose
385
+ * MX resolves back to this server would otherwise mail-loop through the
386
+ * full outbound/inbound pipeline forever.
387
+ */
388
+ public isSelfHostname(hostname: string): boolean {
389
+ const normalize = (name: string) => name.toLowerCase().replace(/\.$/, '');
390
+ const candidate = normalize(hostname);
391
+ if (!candidate) return false;
392
+ return (
393
+ candidate === normalize(this.options.hostname) ||
394
+ candidate === normalize(this.options.outbound?.hostname || '')
395
+ );
396
+ }
397
+
377
398
  /**
378
399
  * Send an outbound email via the Rust SMTP client.
379
400
  * Uses connection pooling in the Rust binary for efficiency.
@@ -453,6 +474,7 @@ export class UnifiedEmailServer extends EventEmitter {
453
474
  await this.initializeDkimAndDns();
454
475
  this.registerBridgeEventHandlers();
455
476
  await this.startSmtpServer();
477
+ this.startDnsValidation();
456
478
  logger.log('info', 'UnifiedEmailServer started successfully');
457
479
  this.emit('started');
458
480
  } catch (error) {
@@ -494,9 +516,14 @@ export class UnifiedEmailServer extends EventEmitter {
494
516
  await this.dkimManager.setupDkimForDomains();
495
517
  logger.log('info', 'DKIM configuration completed for all domains');
496
518
 
497
- const dnsManager = new DnsManager(this.dcRouter);
498
- await dnsManager.ensureDnsRecords(this.domainRegistry.getAllConfigs(), this.dkimCreator);
499
- logger.log('info', 'DNS records ensured for all configured domains');
519
+ if (!this.localDnsProvisioned) {
520
+ await this.dnsManager.provisionLocalDnsRecords(
521
+ this.domainRegistry.getAllConfigs(),
522
+ this.dkimCreator
523
+ );
524
+ this.localDnsProvisioned = true;
525
+ logger.log('info', 'Local DNS records provisioned for all configured domains');
526
+ }
500
527
 
501
528
  this.applyDomainRateLimits();
502
529
  logger.log('info', 'Per-domain rate limits configured');
@@ -505,6 +532,34 @@ export class UnifiedEmailServer extends EventEmitter {
505
532
  logger.log('info', 'DKIM key rotation check completed');
506
533
  }
507
534
 
535
+ private startDnsValidation(): void {
536
+ const generation = ++this.dnsValidationGeneration;
537
+ let task: Promise<void>;
538
+ task = this.dnsManager
539
+ .validateAndReportDnsRecords(this.domainRegistry.getAllConfigs())
540
+ .catch((error) => {
541
+ if (this.dnsValidationGeneration === generation) {
542
+ logger.log('warn', `Background DNS validation did not complete: ${(error as Error).message}`);
543
+ }
544
+ })
545
+ .finally(() => {
546
+ if (this.dnsValidationGeneration === generation && this.dnsValidationTask === task) {
547
+ this.dnsValidationTask = undefined;
548
+ }
549
+ });
550
+ this.dnsValidationTask = task;
551
+ }
552
+
553
+ private async stopDnsValidation(): Promise<void> {
554
+ this.dnsValidationGeneration++;
555
+ const task = this.dnsValidationTask;
556
+ this.dnsValidationTask = undefined;
557
+ this.dnsManager.cancelValidation();
558
+ if (task) {
559
+ await task;
560
+ }
561
+ }
562
+
508
563
  private registerBridgeEventHandlers(): void {
509
564
  this.unregisterBridgeEventHandlers();
510
565
 
@@ -676,6 +731,8 @@ export class UnifiedEmailServer extends EventEmitter {
676
731
  logger.log('info', 'Stopping UnifiedEmailServer');
677
732
 
678
733
  try {
734
+ await this.stopDnsValidation();
735
+
679
736
  // Stop the Rust SMTP server first
680
737
  try {
681
738
  await this.rustBridge.stopSmtpServer();
@@ -1457,6 +1514,16 @@ export class UnifiedEmailServer extends EventEmitter {
1457
1514
  return this.deliveryQueue.listItems();
1458
1515
  }
1459
1516
 
1517
+ /**
1518
+ * Current lifecycle state of the Rust mailer bridge
1519
+ * ('idle' | 'starting' | 'running' | 'restarting' | 'failed' | 'stopped').
1520
+ * Embedders surface this in health/ops endpoints — a bridge in 'failed'
1521
+ * or 'restarting' means mail processing is degraded.
1522
+ */
1523
+ public getBridgeState(): string {
1524
+ return this.rustBridge.state;
1525
+ }
1526
+
1460
1527
  public getQueueItem(id: string): IQueueItem | undefined {
1461
1528
  return this.deliveryQueue.getItem(id);
1462
1529
  }