@serve.zone/dcrouter 17.1.1 → 17.1.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.
- package/.smartconfig.json +2 -2
- package/deno.json +1 -1
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.js +3 -2
- package/dist_ts/dns/classes.dns-server-runtime.js +3 -2
- package/dist_ts/email/classes.email-domain.manager.d.ts +7 -0
- package/dist_ts/email/classes.email-domain.manager.js +147 -70
- package/dist_ts/email/classes.mail-dns-sync.d.ts +5 -0
- package/dist_ts/email/classes.mail-dns-sync.js +29 -5
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/package.json +5 -4
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +4 -1
- package/ts/dns/classes.dns-server-runtime.ts +2 -1
- package/ts/email/classes.email-domain.manager.ts +182 -81
- package/ts/email/classes.mail-dns-sync.ts +28 -4
- package/ts_web/00_commitinfo_data.ts +1 -1
|
@@ -12,6 +12,10 @@ import type {
|
|
|
12
12
|
IEmailDomainOperationError,
|
|
13
13
|
TEmailDomainLifecycleStatus,
|
|
14
14
|
} from '../../ts_interfaces/data/email-domain.js';
|
|
15
|
+
import {
|
|
16
|
+
MAIL_DNS_MAX_RETRY_INTERVAL_MS,
|
|
17
|
+
MAIL_DNS_RETRY_INTERVAL_MS,
|
|
18
|
+
} from './classes.mail-dns-sync.js';
|
|
15
19
|
|
|
16
20
|
export interface IEmailDomainManagerActionResult {
|
|
17
21
|
success: boolean;
|
|
@@ -28,9 +32,20 @@ export interface IEmailDomainManagerActionResult {
|
|
|
28
32
|
|
|
29
33
|
export interface IManagedDkimRepairResult {
|
|
30
34
|
repairedDomainIds: string[];
|
|
35
|
+
repairedConfiguredDomains: string[];
|
|
36
|
+
failedConfiguredDomains: string[];
|
|
31
37
|
failedDomainIds: string[];
|
|
32
38
|
}
|
|
33
39
|
|
|
40
|
+
interface IDkimMaterialRepairResult {
|
|
41
|
+
regenerated: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface IDkimDocumentRepairOutcome {
|
|
45
|
+
outcome?: 'repaired' | 'failed';
|
|
46
|
+
failureMessage?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
34
49
|
/**
|
|
35
50
|
* Persists managed email domains and projects them into SmartMTA. DNS mutation
|
|
36
51
|
* is delegated exclusively to MailDnsSync.
|
|
@@ -38,6 +53,7 @@ export interface IManagedDkimRepairResult {
|
|
|
38
53
|
export class EmailDomainManager {
|
|
39
54
|
private dcRouter: any; // DcRouter — avoids circular import
|
|
40
55
|
private baseEmailDomains: IEmailDomainConfig[] = [];
|
|
56
|
+
private failedBaseDkimDomainNames = new Set<string>();
|
|
41
57
|
private mutationChain: Promise<unknown> = Promise.resolve();
|
|
42
58
|
/** Explicit upstream capability marker; absence is deliberately fail-closed. */
|
|
43
59
|
private get selectorCorrectSigningCapability(): boolean {
|
|
@@ -52,6 +68,10 @@ export class EmailDomainManager {
|
|
|
52
68
|
public setBaseEmailDomains(domains: IEmailDomainConfig[] | undefined): void {
|
|
53
69
|
this.baseEmailDomains = (domains || [])
|
|
54
70
|
.map((domainConfig) => JSON.parse(JSON.stringify(domainConfig)) as IEmailDomainConfig);
|
|
71
|
+
const configuredNames = new Set(this.baseEmailDomains.map((domainConfig) => domainConfig.domain.toLowerCase()));
|
|
72
|
+
this.failedBaseDkimDomainNames = new Set(
|
|
73
|
+
[...this.failedBaseDkimDomainNames].filter((domainName) => configuredNames.has(domainName)),
|
|
74
|
+
);
|
|
55
75
|
}
|
|
56
76
|
|
|
57
77
|
private get dkimCreator(): any | undefined {
|
|
@@ -381,6 +401,8 @@ export class EmailDomainManager {
|
|
|
381
401
|
public async repairManagedDkimBeforeEmailStart(): Promise<IManagedDkimRepairResult> {
|
|
382
402
|
const repairedDomainIds: string[] = [];
|
|
383
403
|
const failedDomainIds: string[] = [];
|
|
404
|
+
const repairedConfiguredDomains: string[] = [];
|
|
405
|
+
const failedConfiguredDomains: string[] = [];
|
|
384
406
|
|
|
385
407
|
await this.runMutationExclusive(async () => {
|
|
386
408
|
const dkimCreator = this.dkimCreator;
|
|
@@ -388,85 +410,10 @@ export class EmailDomainManager {
|
|
|
388
410
|
throw new Error('DKIM key creator is not ready for managed-domain repair');
|
|
389
411
|
}
|
|
390
412
|
|
|
391
|
-
|
|
413
|
+
const managedDocs = await EmailDomainDoc.findAll();
|
|
414
|
+
for (const doc of managedDocs) {
|
|
392
415
|
if (doc.reconciliation?.lifecycleStatus === 'deleting') continue;
|
|
393
|
-
const
|
|
394
|
-
const selector = currentActive?.selector || doc.dkim?.selector || 'default';
|
|
395
|
-
const keySize = currentActive?.keySize || doc.dkim?.keySize || 2048;
|
|
396
|
-
let regenerated = false;
|
|
397
|
-
let outcome: 'repaired' | 'failed' | undefined;
|
|
398
|
-
let failureMessage: string | undefined;
|
|
399
|
-
|
|
400
|
-
try {
|
|
401
|
-
try {
|
|
402
|
-
await dkimCreator.readValidatedDKIMKeysForSelector(doc.domain, selector);
|
|
403
|
-
} catch {
|
|
404
|
-
await dkimCreator.createAndStoreDKIMKeysForSelector(doc.domain, selector, keySize);
|
|
405
|
-
await dkimCreator.readValidatedDKIMKeysForSelector(doc.domain, selector);
|
|
406
|
-
regenerated = true;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
const record = await dkimCreator.getDNSRecordForSelector(doc.domain, selector);
|
|
410
|
-
const publicKey = this.extractDkimPublicKey(record?.value);
|
|
411
|
-
if (!publicKey) {
|
|
412
|
-
throw new Error('validated DKIM material produced no usable public key');
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
if (
|
|
416
|
-
!regenerated
|
|
417
|
-
&& !this.hasDkimRepairFailure(doc)
|
|
418
|
-
&& currentActive?.selector === selector
|
|
419
|
-
&& currentActive.publicKey === publicKey
|
|
420
|
-
&& doc.dkim?.selector === selector
|
|
421
|
-
&& doc.dkim.publicKey === publicKey
|
|
422
|
-
) {
|
|
423
|
-
continue;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
const now = new Date().toISOString();
|
|
427
|
-
doc.activeDkim = {
|
|
428
|
-
selector,
|
|
429
|
-
keySize,
|
|
430
|
-
publicKey,
|
|
431
|
-
createdAt: regenerated ? now : currentActive?.createdAt || now,
|
|
432
|
-
validatedAt: now,
|
|
433
|
-
};
|
|
434
|
-
doc.dkim = {
|
|
435
|
-
...doc.dkim,
|
|
436
|
-
selector,
|
|
437
|
-
keySize,
|
|
438
|
-
publicKey,
|
|
439
|
-
...(regenerated ? { lastRotatedAt: now } : {}),
|
|
440
|
-
};
|
|
441
|
-
doc.dnsStatus = {
|
|
442
|
-
...doc.dnsStatus,
|
|
443
|
-
dkim: 'unchecked',
|
|
444
|
-
};
|
|
445
|
-
doc.reconciliation = this.invalidateDkimReconciliation(
|
|
446
|
-
doc,
|
|
447
|
-
regenerated ? 'DKIM_KEY_MATERIAL_REGENERATED' : 'DKIM_KEY_MATERIAL_REPAIRED',
|
|
448
|
-
regenerated
|
|
449
|
-
? 'Caller-managed DKIM key material was regenerated; DNS deployment must be revalidated'
|
|
450
|
-
: 'Caller-managed DKIM metadata was repaired; DNS deployment must be revalidated',
|
|
451
|
-
);
|
|
452
|
-
doc.updatedAt = now;
|
|
453
|
-
outcome = 'repaired';
|
|
454
|
-
} catch (error: unknown) {
|
|
455
|
-
const message = `Failed to repair caller-managed DKIM for ${selector}._domainkey.${doc.domain}: ${(error as Error).message}`;
|
|
456
|
-
doc.reconciliation = this.invalidateDkimReconciliation(
|
|
457
|
-
doc,
|
|
458
|
-
'DKIM_KEY_REPAIR_FAILED',
|
|
459
|
-
message,
|
|
460
|
-
false,
|
|
461
|
-
);
|
|
462
|
-
doc.dnsStatus = {
|
|
463
|
-
...doc.dnsStatus,
|
|
464
|
-
dkim: 'invalid',
|
|
465
|
-
};
|
|
466
|
-
doc.updatedAt = new Date().toISOString();
|
|
467
|
-
outcome = 'failed';
|
|
468
|
-
failureMessage = message;
|
|
469
|
-
}
|
|
416
|
+
const { outcome, failureMessage } = await this.repairManagedDkimDocument(doc, dkimCreator);
|
|
470
417
|
|
|
471
418
|
// Persistence failures are global prerequisites, not per-domain key
|
|
472
419
|
// failures. Keep this save outside the material-repair catch.
|
|
@@ -479,6 +426,28 @@ export class EmailDomainManager {
|
|
|
479
426
|
}
|
|
480
427
|
}
|
|
481
428
|
|
|
429
|
+
const managedNames = new Set(
|
|
430
|
+
managedDocs
|
|
431
|
+
.filter((doc) => doc.reconciliation?.lifecycleStatus !== 'deleting')
|
|
432
|
+
.map((doc) => doc.domain.toLowerCase()),
|
|
433
|
+
);
|
|
434
|
+
for (const domainConfig of this.baseEmailDomains) {
|
|
435
|
+
const domainName = domainConfig.domain.toLowerCase();
|
|
436
|
+
if (managedNames.has(domainName)) continue;
|
|
437
|
+
const selector = domainConfig.dkim?.selector || 'default';
|
|
438
|
+
const keySize = domainConfig.dkim?.keySize || 2048;
|
|
439
|
+
try {
|
|
440
|
+
const result = await this.ensureValidatedDkimMaterial(domainName, selector, keySize, dkimCreator);
|
|
441
|
+
const wasFailed = this.failedBaseDkimDomainNames.delete(domainName);
|
|
442
|
+
if (result.regenerated || wasFailed) repairedConfiguredDomains.push(domainName);
|
|
443
|
+
} catch (error: unknown) {
|
|
444
|
+
const message = `Failed to repair caller-managed DKIM for ${selector}._domainkey.${domainName}: ${(error as Error).message}`;
|
|
445
|
+
this.failedBaseDkimDomainNames.add(domainName);
|
|
446
|
+
failedConfiguredDomains.push(domainName);
|
|
447
|
+
logger.log('error', message);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
482
451
|
await this.syncManagedDomainsToRuntime({ excludeDkimRepairFailures: true });
|
|
483
452
|
});
|
|
484
453
|
|
|
@@ -488,7 +457,137 @@ export class EmailDomainManager {
|
|
|
488
457
|
[...new Set(repairedDomainIds)],
|
|
489
458
|
);
|
|
490
459
|
}
|
|
491
|
-
return {
|
|
460
|
+
return {
|
|
461
|
+
repairedDomainIds,
|
|
462
|
+
repairedConfiguredDomains,
|
|
463
|
+
failedConfiguredDomains,
|
|
464
|
+
failedDomainIds,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** Retry a previously failed managed-domain repair when its bounded retry is due. */
|
|
469
|
+
public async repairManagedDkimIfDue(id: string, now = Date.now()): Promise<EmailDomainDoc | undefined> {
|
|
470
|
+
return await this.runMutationExclusive(async () => {
|
|
471
|
+
const doc = await EmailDomainDoc.findById(id);
|
|
472
|
+
if (!doc || !this.hasDkimRepairFailure(doc)) return doc || undefined;
|
|
473
|
+
const retryAt = Date.parse(doc.reconciliation?.retryAt || '');
|
|
474
|
+
if (Number.isFinite(retryAt) && retryAt > now) return doc;
|
|
475
|
+
const dkimCreator = this.dkimCreator;
|
|
476
|
+
if (!dkimCreator) return doc;
|
|
477
|
+
|
|
478
|
+
const result = await this.repairManagedDkimDocument(doc, dkimCreator);
|
|
479
|
+
await doc.save();
|
|
480
|
+
await this.syncManagedDomainsToRuntime({ excludeDkimRepairFailures: true });
|
|
481
|
+
if (result.outcome === 'failed') logger.log('error', result.failureMessage!);
|
|
482
|
+
return doc;
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
private async repairManagedDkimDocument(
|
|
487
|
+
doc: EmailDomainDoc,
|
|
488
|
+
dkimCreator: any,
|
|
489
|
+
): Promise<IDkimDocumentRepairOutcome> {
|
|
490
|
+
const currentActive = this.getActiveDkim(doc);
|
|
491
|
+
const selector = currentActive?.selector || doc.dkim?.selector || 'default';
|
|
492
|
+
const keySize = currentActive?.keySize || doc.dkim?.keySize || 2048;
|
|
493
|
+
try {
|
|
494
|
+
const { regenerated } = await this.ensureValidatedDkimMaterial(
|
|
495
|
+
doc.domain,
|
|
496
|
+
selector,
|
|
497
|
+
keySize,
|
|
498
|
+
dkimCreator,
|
|
499
|
+
);
|
|
500
|
+
const record = await dkimCreator.getDNSRecordForSelector(doc.domain, selector);
|
|
501
|
+
const publicKey = this.extractDkimPublicKey(record?.value);
|
|
502
|
+
if (!publicKey) throw new Error('validated DKIM material produced no usable public key');
|
|
503
|
+
|
|
504
|
+
if (
|
|
505
|
+
!regenerated
|
|
506
|
+
&& !this.hasDkimRepairFailure(doc)
|
|
507
|
+
&& currentActive?.selector === selector
|
|
508
|
+
&& currentActive.publicKey === publicKey
|
|
509
|
+
&& doc.dkim?.selector === selector
|
|
510
|
+
&& doc.dkim.publicKey === publicKey
|
|
511
|
+
) return {};
|
|
512
|
+
|
|
513
|
+
const timestamp = new Date().toISOString();
|
|
514
|
+
doc.activeDkim = {
|
|
515
|
+
selector,
|
|
516
|
+
keySize,
|
|
517
|
+
publicKey,
|
|
518
|
+
createdAt: regenerated ? timestamp : currentActive?.createdAt || timestamp,
|
|
519
|
+
validatedAt: timestamp,
|
|
520
|
+
};
|
|
521
|
+
doc.dkim = {
|
|
522
|
+
...doc.dkim,
|
|
523
|
+
selector,
|
|
524
|
+
keySize,
|
|
525
|
+
publicKey,
|
|
526
|
+
...(regenerated ? { lastRotatedAt: timestamp } : {}),
|
|
527
|
+
};
|
|
528
|
+
doc.dnsStatus = { ...doc.dnsStatus, dkim: 'unchecked' };
|
|
529
|
+
doc.reconciliation = this.invalidateDkimReconciliation(
|
|
530
|
+
doc,
|
|
531
|
+
regenerated ? 'DKIM_KEY_MATERIAL_REGENERATED' : 'DKIM_KEY_MATERIAL_REPAIRED',
|
|
532
|
+
regenerated
|
|
533
|
+
? 'Caller-managed DKIM key material was regenerated; DNS deployment must be revalidated'
|
|
534
|
+
: 'Caller-managed DKIM metadata was repaired; DNS deployment must be revalidated',
|
|
535
|
+
);
|
|
536
|
+
doc.updatedAt = timestamp;
|
|
537
|
+
return { outcome: 'repaired' };
|
|
538
|
+
} catch (error: unknown) {
|
|
539
|
+
const message = `Failed to repair caller-managed DKIM for ${selector}._domainkey.${doc.domain}: ${(error as Error).message}`;
|
|
540
|
+
const previousAttempt = doc.reconciliation?.retryAttempt || 0;
|
|
541
|
+
const retryAttempt = previousAttempt + 1;
|
|
542
|
+
const retryDelay = Math.min(
|
|
543
|
+
MAIL_DNS_RETRY_INTERVAL_MS * (2 ** Math.max(0, retryAttempt - 1)),
|
|
544
|
+
MAIL_DNS_MAX_RETRY_INTERVAL_MS,
|
|
545
|
+
);
|
|
546
|
+
doc.reconciliation = this.invalidateDkimReconciliation(doc, 'DKIM_KEY_REPAIR_FAILED', message);
|
|
547
|
+
doc.reconciliation.retryAttempt = retryAttempt;
|
|
548
|
+
doc.reconciliation.retryAt = new Date(Date.now() + retryDelay).toISOString();
|
|
549
|
+
doc.dnsStatus = { ...doc.dnsStatus, dkim: 'invalid' };
|
|
550
|
+
doc.updatedAt = new Date().toISOString();
|
|
551
|
+
return { outcome: 'failed', failureMessage: message };
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
private async ensureValidatedDkimMaterial(
|
|
556
|
+
domain: string,
|
|
557
|
+
selector: string,
|
|
558
|
+
keySize: number,
|
|
559
|
+
dkimCreator: any,
|
|
560
|
+
): Promise<IDkimMaterialRepairResult> {
|
|
561
|
+
let initialError: unknown;
|
|
562
|
+
try {
|
|
563
|
+
await dkimCreator.readValidatedDKIMKeysForSelector(domain, selector);
|
|
564
|
+
return { regenerated: false };
|
|
565
|
+
} catch (error: unknown) {
|
|
566
|
+
initialError = error;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
let creationError: unknown;
|
|
570
|
+
try {
|
|
571
|
+
await dkimCreator.createAndStoreDKIMKeysForSelector(domain, selector, keySize);
|
|
572
|
+
} catch (error: unknown) {
|
|
573
|
+
creationError = error;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
try {
|
|
577
|
+
await dkimCreator.readValidatedDKIMKeysForSelector(domain, selector);
|
|
578
|
+
// Creation may have stored a complete key pair before a metadata write
|
|
579
|
+
// failed. Treat any attempted regeneration as a DNS-changing operation.
|
|
580
|
+
return { regenerated: true };
|
|
581
|
+
} catch (finalError: unknown) {
|
|
582
|
+
const parts = [
|
|
583
|
+
`initial validation failed: ${(initialError as Error).message}`,
|
|
584
|
+
creationError
|
|
585
|
+
? `regeneration failed: ${(creationError as Error).message}`
|
|
586
|
+
: 'regeneration completed',
|
|
587
|
+
`final validation failed: ${(finalError as Error).message}`,
|
|
588
|
+
];
|
|
589
|
+
throw new Error(parts.join('; '));
|
|
590
|
+
}
|
|
492
591
|
}
|
|
493
592
|
|
|
494
593
|
public async requestDkimRotation(id: string): Promise<IEmailDomainManagerActionResult> {
|
|
@@ -623,6 +722,7 @@ export class EmailDomainManager {
|
|
|
623
722
|
errors: _errors,
|
|
624
723
|
lastSuccessAt: _lastSuccessAt,
|
|
625
724
|
retryAt: _retryAt,
|
|
725
|
+
retryAttempt: _retryAttempt,
|
|
626
726
|
...preserved
|
|
627
727
|
} = previous;
|
|
628
728
|
const alreadyInvalidated = (
|
|
@@ -763,9 +863,10 @@ export class EmailDomainManager {
|
|
|
763
863
|
const mergedDomains = new Map<string, IEmailDomainConfig>();
|
|
764
864
|
for (const domainConfig of this.baseEmailDomains) {
|
|
765
865
|
const key = domainConfig.domain.toLowerCase();
|
|
766
|
-
|
|
866
|
+
const dkimRepairFailed = failedDomainNames.has(key) || this.failedBaseDkimDomainNames.has(key);
|
|
867
|
+
if (dkimRepairFailed && optionsArg.excludeDkimRepairFailures) continue;
|
|
767
868
|
const projectedConfig = JSON.parse(JSON.stringify(domainConfig)) as IEmailDomainConfig;
|
|
768
|
-
if (
|
|
869
|
+
if (dkimRepairFailed) delete projectedConfig.dkim;
|
|
769
870
|
mergedDomains.set(key, projectedConfig);
|
|
770
871
|
}
|
|
771
872
|
for (const managedConfig of await this.buildManagedDomainConfigs(optionsArg)) {
|
|
@@ -32,6 +32,11 @@ export const MAIL_DNS_SYNC_CREATED_BY = 'mail-dns-reconciler';
|
|
|
32
32
|
export const MAIL_DNS_MANAGED_BY = 'mail-dns-reconciler';
|
|
33
33
|
export const MAIL_DNS_RETRY_INTERVAL_MS = 5 * 60_000;
|
|
34
34
|
export const MAIL_DNS_MAX_RETRY_INTERVAL_MS = 60 * 60_000;
|
|
35
|
+
export const MAIL_DNS_PUBLIC_RESOLVER_OPTIONS = {
|
|
36
|
+
strategy: 'doh' as const,
|
|
37
|
+
allowDohFallback: false,
|
|
38
|
+
timeoutMs: 5_000,
|
|
39
|
+
};
|
|
35
40
|
|
|
36
41
|
export interface IMailDnsVerificationResult {
|
|
37
42
|
valid: boolean;
|
|
@@ -150,7 +155,7 @@ export class SmartDnsMailVerifier implements IMailDnsVerifier {
|
|
|
150
155
|
private client?: plugins.smartdns.dnsClientMod.Smartdns;
|
|
151
156
|
|
|
152
157
|
private getClient(): plugins.smartdns.dnsClientMod.Smartdns {
|
|
153
|
-
this.client ||= new plugins.smartdns.dnsClientMod.Smartdns(
|
|
158
|
+
this.client ||= new plugins.smartdns.dnsClientMod.Smartdns(MAIL_DNS_PUBLIC_RESOLVER_OPTIONS);
|
|
154
159
|
return this.client;
|
|
155
160
|
}
|
|
156
161
|
|
|
@@ -476,8 +481,20 @@ export class MailDnsSync {
|
|
|
476
481
|
const plans: IPlannedDomain[] = [];
|
|
477
482
|
const deletingDocs: EmailDomainDoc[] = [];
|
|
478
483
|
for (const loadedDoc of docs) {
|
|
479
|
-
|
|
484
|
+
if (loadedDoc.reconciliation?.lifecycleStatus === 'deleting') {
|
|
485
|
+
await this.persistDeletingAttempt(loadedDoc);
|
|
486
|
+
deletingDocs.push(loadedDoc);
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
const repairedDoc = await this.dcRouterRef.emailDomainManager?.repairManagedDkimIfDue?.(loadedDoc.id)
|
|
480
490
|
|| loadedDoc;
|
|
491
|
+
if (repairedDoc.reconciliation?.errors.some((error) => error.code === 'DKIM_KEY_REPAIR_FAILED')) {
|
|
492
|
+
// Key repair owns this retry schedule. DNS planning here would publish
|
|
493
|
+
// stale DKIM material and overwrite the bounded repair backoff.
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
const doc = await this.dcRouterRef.emailDomainManager?.stageDkimRotationIfDue?.(repairedDoc.id)
|
|
497
|
+
|| repairedDoc;
|
|
481
498
|
if (doc.reconciliation?.lifecycleStatus === 'deleting') {
|
|
482
499
|
await this.persistDeletingAttempt(doc);
|
|
483
500
|
deletingDocs.push(doc);
|
|
@@ -1243,9 +1260,16 @@ export class MailDnsSync {
|
|
|
1243
1260
|
? {}
|
|
1244
1261
|
: { retryAt: new Date(Date.now() + retryDelay).toISOString() }),
|
|
1245
1262
|
};
|
|
1263
|
+
const mailPublicationReady = plan.egressIdentitySupported
|
|
1264
|
+
&& plan.revision.edgeIdentities.length > 0
|
|
1265
|
+
&& plan.revision.mxSlots.length > 0;
|
|
1246
1266
|
plan.doc.dnsStatus = {
|
|
1247
|
-
mx:
|
|
1248
|
-
|
|
1267
|
+
mx: mailPublicationReady
|
|
1268
|
+
? this.statusForIntent(plan.intents, (intent) => intent.type === 'MX')
|
|
1269
|
+
: 'unchecked',
|
|
1270
|
+
spf: mailPublicationReady
|
|
1271
|
+
? this.statusForIntent(plan.intents, (intent) => intent.type === 'TXT' && intent.value.startsWith('v=spf1'))
|
|
1272
|
+
: 'unchecked',
|
|
1249
1273
|
dkim: this.statusForIntent(plan.intents, (intent) => intent.type === 'TXT' && intent.name.includes('._domainkey.')),
|
|
1250
1274
|
dmarc: this.statusForIntent(plan.intents, (intent) => intent.type === 'TXT' && intent.value.startsWith('v=DMARC1')),
|
|
1251
1275
|
lastCheckedAt: now,
|