@serve.zone/dcrouter 17.0.0 → 17.1.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.
- package/.smartconfig.json +7 -4
- package/deno.json +1 -1
- package/dist_serve/bundle.js +1316 -1151
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +3 -1
- package/dist_ts/classes.dcrouter.js +113 -52
- package/dist_ts/db/classes.cache.cleaner.d.ts +3 -0
- package/dist_ts/db/classes.cache.cleaner.js +7 -3
- package/dist_ts/db/classes.dcrouter-db.js +1 -2
- package/dist_ts/db/documents/classes.cached.email.d.ts +41 -4
- package/dist_ts/db/documents/classes.cached.email.js +36 -5
- package/dist_ts/db/documents/classes.dns-record.doc.js +3 -1
- package/dist_ts/dns/classes.dns-server-runtime.d.ts +0 -1
- package/dist_ts/dns/classes.dns-server-runtime.js +4 -36
- package/dist_ts/dns/index.d.ts +1 -0
- package/dist_ts/dns/index.js +2 -1
- package/dist_ts/dns/manager.dns.d.ts +1 -0
- package/dist_ts/dns/manager.dns.js +49 -14
- package/dist_ts/dns/providers/cloudflare.provider.js +19 -4
- package/dist_ts/dns/txt-record-presentation.d.ts +20 -0
- package/dist_ts/dns/txt-record-presentation.js +131 -0
- package/dist_ts/email/classes.accepted-email-spool.d.ts +32 -3
- package/dist_ts/email/classes.accepted-email-spool.js +174 -13
- package/dist_ts/email/classes.email-domain.manager.d.ts +14 -1
- package/dist_ts/email/classes.email-domain.manager.js +151 -8
- package/dist_ts/email/classes.email-settings.manager.js +2 -2
- package/dist_ts/email/classes.mail-dns-sync.d.ts +31 -5
- package/dist_ts/email/classes.mail-dns-sync.js +508 -117
- package/dist_ts/email/classes.smartmta-blob-storage-manager.d.ts +2 -2
- package/dist_ts/email/classes.smartmta-blob-storage-manager.js +4 -3
- package/dist_ts/email/classes.workapp-mail-manager.d.ts +1 -0
- package/dist_ts/email/classes.workapp-mail-manager.js +26 -2
- package/dist_ts/email/index.d.ts +2 -0
- package/dist_ts/email/index.js +3 -1
- package/dist_ts/email/mail-dns-txt-policy.d.ts +18 -0
- package/dist_ts/email/mail-dns-txt-policy.js +466 -0
- package/dist_ts/email/reconcile-mail-dns-singleton.d.ts +47 -0
- package/dist_ts/email/reconcile-mail-dns-singleton.js +159 -0
- package/dist_ts/opsserver/classes.opsserver.d.ts +8 -0
- package/dist_ts/opsserver/classes.opsserver.js +34 -1
- package/dist_ts/opsserver/handlers/certificate.handler.js +25 -4
- package/dist_ts/opsserver/handlers/config.handler.js +9 -5
- package/dist_ts/opsserver/handlers/dns-provider.handler.js +19 -2
- package/dist_ts/opsserver/handlers/dns-record.handler.js +25 -4
- package/dist_ts/opsserver/handlers/domain.handler.js +37 -3
- package/dist_ts/opsserver/handlers/email-domain.handler.js +5 -7
- package/dist_ts/opsserver/handlers/email-ops.handler.d.ts +1 -0
- package/dist_ts/opsserver/handlers/email-ops.handler.js +26 -7
- package/dist_ts/opsserver/handlers/index.d.ts +1 -0
- package/dist_ts/opsserver/handlers/index.js +2 -1
- package/dist_ts/opsserver/handlers/logs.handler.d.ts +1 -1
- package/dist_ts/opsserver/handlers/logs.handler.js +3 -3
- package/dist_ts/opsserver/handlers/realtime.handler.d.ts +37 -0
- package/dist_ts/opsserver/handlers/realtime.handler.js +154 -0
- package/dist_ts/opsserver/handlers/remoteingress.handler.js +22 -1
- package/dist_ts/opsserver/handlers/route-management.handler.js +47 -5
- package/dist_ts/opsserver/handlers/stats.handler.js +5 -1
- package/dist_ts/paths.d.ts +0 -11
- package/dist_ts/paths.js +1 -22
- package/dist_ts/plugins.d.ts +0 -34
- package/dist_ts/plugins.js +1 -62
- package/dist_ts_interfaces/data/email-domain.d.ts +14 -0
- package/dist_ts_interfaces/requests/config.d.ts +1 -1
- package/dist_ts_interfaces/requests/email-ops.d.ts +34 -1
- package/dist_ts_interfaces/requests/index.d.ts +1 -0
- package/dist_ts_interfaces/requests/index.js +2 -1
- package/dist_ts_interfaces/requests/realtime.d.ts +38 -0
- package/dist_ts_interfaces/requests/realtime.js +11 -0
- package/dist_ts_migrations/index.d.ts +6 -0
- package/dist_ts_migrations/index.js +208 -9
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate/certificates.js +9 -2
- package/dist_ts_web/appstate/domains.js +16 -5
- package/dist_ts_web/appstate/email-domains.d.ts +3 -0
- package/dist_ts_web/appstate/email-domains.js +47 -14
- package/dist_ts_web/appstate/email-ops.d.ts +1 -0
- package/dist_ts_web/appstate/email-ops.js +20 -2
- package/dist_ts_web/appstate/realtime.d.ts +27 -0
- package/dist_ts_web/appstate/realtime.js +71 -0
- package/dist_ts_web/appstate/remoteingress.js +9 -3
- package/dist_ts_web/appstate/routes.js +16 -5
- package/dist_ts_web/appstate/runtime.js +135 -7
- package/dist_ts_web/appstate.d.ts +1 -0
- package/dist_ts_web/appstate.js +2 -1
- package/dist_ts_web/elements/email/email-domain-creation-progress.d.ts +33 -0
- package/dist_ts_web/elements/email/email-domain-creation-progress.js +257 -0
- package/dist_ts_web/elements/email/ops-view-email-domains.js +233 -68
- package/dist_ts_web/elements/email/ops-view-emails.d.ts +4 -2
- package/dist_ts_web/elements/email/ops-view-emails.js +25 -13
- package/dist_ts_web/elements/overview/ops-view-config.js +5 -5
- package/package.json +22 -19
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +124 -57
- package/ts/db/classes.cache.cleaner.ts +18 -3
- package/ts/db/classes.dcrouter-db.ts +0 -2
- package/ts/db/documents/classes.cached.email.ts +78 -4
- package/ts/db/documents/classes.dns-record.doc.ts +1 -0
- package/ts/dns/classes.dns-server-runtime.ts +9 -45
- package/ts/dns/index.ts +1 -0
- package/ts/dns/manager.dns.ts +50 -16
- package/ts/dns/providers/cloudflare.provider.ts +21 -3
- package/ts/dns/txt-record-presentation.ts +134 -0
- package/ts/email/classes.accepted-email-spool.ts +220 -14
- package/ts/email/classes.email-domain.manager.ts +201 -7
- package/ts/email/classes.email-settings.manager.ts +1 -1
- package/ts/email/classes.mail-dns-sync.ts +588 -122
- package/ts/email/classes.smartmta-blob-storage-manager.ts +3 -2
- package/ts/email/classes.workapp-mail-manager.ts +34 -1
- package/ts/email/index.ts +2 -0
- package/ts/email/mail-dns-txt-policy.ts +505 -0
- package/ts/email/reconcile-mail-dns-singleton.ts +224 -0
- package/ts/opsserver/classes.opsserver.ts +42 -0
- package/ts/opsserver/handlers/certificate.handler.ts +24 -3
- package/ts/opsserver/handlers/config.handler.ts +9 -5
- package/ts/opsserver/handlers/dns-provider.handler.ts +18 -1
- package/ts/opsserver/handlers/dns-record.handler.ts +24 -3
- package/ts/opsserver/handlers/domain.handler.ts +36 -2
- package/ts/opsserver/handlers/email-domain.handler.ts +4 -5
- package/ts/opsserver/handlers/email-ops.handler.ts +29 -6
- package/ts/opsserver/handlers/index.ts +1 -0
- package/ts/opsserver/handlers/logs.handler.ts +2 -2
- package/ts/opsserver/handlers/realtime.handler.ts +222 -0
- package/ts/opsserver/handlers/remoteingress.handler.ts +21 -0
- package/ts/opsserver/handlers/route-management.handler.ts +46 -4
- package/ts/opsserver/handlers/stats.handler.ts +4 -0
- package/ts/paths.ts +0 -25
- package/ts/plugins.ts +0 -68
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate/certificates.ts +6 -1
- package/ts_web/appstate/domains.ts +11 -4
- package/ts_web/appstate/email-domains.ts +68 -14
- package/ts_web/appstate/email-ops.ts +20 -1
- package/ts_web/appstate/realtime.ts +109 -0
- package/ts_web/appstate/remoteingress.ts +6 -2
- package/ts_web/appstate/routes.ts +11 -4
- package/ts_web/appstate/runtime.ts +176 -7
- package/ts_web/appstate.ts +1 -0
- package/ts_web/elements/email/email-domain-creation-progress.ts +340 -0
- package/ts_web/elements/email/ops-view-email-domains.ts +273 -74
- package/ts_web/elements/email/ops-view-emails.ts +23 -16
- package/ts_web/elements/overview/ops-view-config.ts +4 -4
|
@@ -1,7 +1,10 @@
|
|
|
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 {
|
|
4
|
+
import type {
|
|
5
|
+
ICachedEmailSmtpTransaction,
|
|
6
|
+
TCachedEmailDirection,
|
|
7
|
+
} from '../db/documents/classes.cached.email.js';
|
|
5
8
|
import type {
|
|
6
9
|
Email,
|
|
7
10
|
IExtendedSmtpSession,
|
|
@@ -19,6 +22,7 @@ const ACCEPTED_EMAIL_SPOOL_BATCH_SIZE = 25;
|
|
|
19
22
|
const ACCEPTED_EMAIL_STOP_DRAIN_TIMEOUT_MS = 30_000;
|
|
20
23
|
|
|
21
24
|
export type TSmartMtaQueueItemLike = {
|
|
25
|
+
id?: string;
|
|
22
26
|
processingResult?: {
|
|
23
27
|
headers?: Record<string, string>;
|
|
24
28
|
email?: { headers?: Record<string, string> };
|
|
@@ -27,8 +31,20 @@ export type TSmartMtaQueueItemLike = {
|
|
|
27
31
|
attempts?: number;
|
|
28
32
|
nextAttempt?: Date;
|
|
29
33
|
lastError?: string;
|
|
34
|
+
smtpTransactions?: ICachedEmailSmtpTransaction[];
|
|
30
35
|
};
|
|
31
36
|
|
|
37
|
+
export interface IEmailQueuePersistedEvent {
|
|
38
|
+
cachedEmailId: string;
|
|
39
|
+
status: string;
|
|
40
|
+
reason: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ISmtpTransactionPersistedEvent {
|
|
44
|
+
cachedEmailId: string;
|
|
45
|
+
transaction: ICachedEmailSmtpTransaction;
|
|
46
|
+
}
|
|
47
|
+
|
|
32
48
|
type TSmartMtaQueueReader = UnifiedEmailServer & {
|
|
33
49
|
getQueueItems?: () => TSmartMtaQueueItemLike[];
|
|
34
50
|
};
|
|
@@ -86,6 +102,13 @@ export class AcceptedEmailSpool {
|
|
|
86
102
|
private processing = false;
|
|
87
103
|
private stopping = false;
|
|
88
104
|
private queueUpdatePromises = new Set<Promise<void>>();
|
|
105
|
+
private cachedEmailUpdateChains = new Map<string, Promise<void>>();
|
|
106
|
+
private smtpTransactionListeners = new Set<(
|
|
107
|
+
eventArg: ISmtpTransactionPersistedEvent,
|
|
108
|
+
) => void | Promise<void>>();
|
|
109
|
+
private emailQueuePersistedListeners = new Set<(
|
|
110
|
+
eventArg: IEmailQueuePersistedEvent,
|
|
111
|
+
) => void | Promise<void>>();
|
|
89
112
|
|
|
90
113
|
constructor(private dcRouterRef: DcRouter) {}
|
|
91
114
|
|
|
@@ -118,7 +141,8 @@ export class AcceptedEmailSpool {
|
|
|
118
141
|
cachedEmail.cc = Array.isArray(email.cc) ? email.cc : [];
|
|
119
142
|
cachedEmail.bcc = Array.isArray(email.bcc) ? email.bcc : [];
|
|
120
143
|
cachedEmail.subject = email.subject || '';
|
|
121
|
-
|
|
144
|
+
const persistedRawMessage = this.setDcRouterCacheIdHeader(rawMessage.toString('utf8'), cachedEmail.id);
|
|
145
|
+
await this.persistRawMessage(cachedEmail, persistedRawMessage);
|
|
122
146
|
cachedEmail.status = 'pending';
|
|
123
147
|
cachedEmail.nextAttempt = new Date();
|
|
124
148
|
cachedEmail.direction = session.authenticated ? 'outbound' : 'inbound';
|
|
@@ -137,10 +161,17 @@ export class AcceptedEmailSpool {
|
|
|
137
161
|
});
|
|
138
162
|
cachedEmail.updateSenderDomain();
|
|
139
163
|
cachedEmail.updateRecipientDomains();
|
|
140
|
-
|
|
164
|
+
try {
|
|
165
|
+
await cachedEmail.save();
|
|
166
|
+
await this.notifyEmailQueuePersisted(cachedEmail, 'email-accepted');
|
|
167
|
+
} catch (error) {
|
|
168
|
+
await this.deleteRawMessage(cachedEmail).catch(() => undefined);
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
141
171
|
if (context.abortSignal?.aborted) {
|
|
142
172
|
cachedEmail.markFailed('Message acceptance aborted before SMTP success');
|
|
143
173
|
await cachedEmail.save();
|
|
174
|
+
await this.notifyEmailQueuePersisted(cachedEmail, 'email-acceptance-aborted');
|
|
144
175
|
throw new Error('Message acceptance aborted before SMTP success');
|
|
145
176
|
}
|
|
146
177
|
|
|
@@ -149,6 +180,7 @@ export class AcceptedEmailSpool {
|
|
|
149
180
|
} else {
|
|
150
181
|
cachedEmail.markDelivered();
|
|
151
182
|
await cachedEmail.save();
|
|
183
|
+
await this.notifyEmailQueuePersisted(cachedEmail, 'email-accepted-without-queue');
|
|
152
184
|
}
|
|
153
185
|
|
|
154
186
|
return {
|
|
@@ -174,7 +206,8 @@ export class AcceptedEmailSpool {
|
|
|
174
206
|
cachedEmail.cc = [];
|
|
175
207
|
cachedEmail.bcc = [];
|
|
176
208
|
cachedEmail.subject = optionsArg.subject || this.extractHeader(rawMessage, 'subject') || '';
|
|
177
|
-
|
|
209
|
+
const persistedRawMessage = this.setDcRouterCacheIdHeader(rawMessage, cachedEmail.id);
|
|
210
|
+
await this.persistRawMessage(cachedEmail, persistedRawMessage);
|
|
178
211
|
cachedEmail.status = 'pending';
|
|
179
212
|
cachedEmail.nextAttempt = new Date();
|
|
180
213
|
cachedEmail.direction = optionsArg.direction
|
|
@@ -194,13 +227,20 @@ export class AcceptedEmailSpool {
|
|
|
194
227
|
});
|
|
195
228
|
cachedEmail.updateSenderDomain();
|
|
196
229
|
cachedEmail.updateRecipientDomains();
|
|
197
|
-
|
|
230
|
+
try {
|
|
231
|
+
await cachedEmail.save();
|
|
232
|
+
await this.notifyEmailQueuePersisted(cachedEmail, 'raw-email-accepted');
|
|
233
|
+
} catch (error) {
|
|
234
|
+
await this.deleteRawMessage(cachedEmail).catch(() => undefined);
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
198
237
|
|
|
199
238
|
if (optionsArg.processAfterAccept !== false) {
|
|
200
239
|
this.run();
|
|
201
240
|
} else {
|
|
202
241
|
cachedEmail.markDelivered();
|
|
203
242
|
await cachedEmail.save();
|
|
243
|
+
await this.notifyEmailQueuePersisted(cachedEmail, 'raw-email-accepted-without-queue');
|
|
204
244
|
}
|
|
205
245
|
|
|
206
246
|
return {
|
|
@@ -268,14 +308,73 @@ export class AcceptedEmailSpool {
|
|
|
268
308
|
item: TSmartMtaQueueItemLike,
|
|
269
309
|
status: 'queued' | 'deferred' | 'delivered' | 'failed',
|
|
270
310
|
failureMessage: string,
|
|
271
|
-
): void {
|
|
272
|
-
const
|
|
311
|
+
): Promise<void> {
|
|
312
|
+
const cachedEmailId = this.getCachedEmailIdFromQueueItem(item);
|
|
313
|
+
const updatePromise = this.runCachedEmailUpdate(cachedEmailId, async () => {
|
|
314
|
+
await this.updateAcceptedEmailFromQueueItem(item, status);
|
|
315
|
+
}).catch((error) => {
|
|
273
316
|
logger.log('warn', `${failureMessage}: ${(error as Error).message}`);
|
|
274
317
|
});
|
|
275
|
-
this.
|
|
276
|
-
|
|
277
|
-
|
|
318
|
+
this.trackUpdatePromise(updatePromise);
|
|
319
|
+
return updatePromise;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
public trackSmtpTransaction(
|
|
323
|
+
transactionArg: ICachedEmailSmtpTransaction,
|
|
324
|
+
emailServerArg: UnifiedEmailServer,
|
|
325
|
+
): Promise<void> {
|
|
326
|
+
const queueItem = emailServerArg.getQueueItem(
|
|
327
|
+
transactionArg.queueItemId,
|
|
328
|
+
) as TSmartMtaQueueItemLike | undefined;
|
|
329
|
+
const cachedEmailId = queueItem ? this.getCachedEmailIdFromQueueItem(queueItem) : undefined;
|
|
330
|
+
const updatePromise = this.runCachedEmailUpdate(cachedEmailId, async () => {
|
|
331
|
+
if (!cachedEmailId || !this.dcRouterRef.dcRouterDb?.isReady()) return;
|
|
332
|
+
const cachedEmail = await CachedEmail.findById(cachedEmailId);
|
|
333
|
+
if (!cachedEmail) return;
|
|
334
|
+
this.appendSmtpTransaction(cachedEmail, transactionArg);
|
|
335
|
+
await cachedEmail.save();
|
|
336
|
+
const event: ISmtpTransactionPersistedEvent = {
|
|
337
|
+
cachedEmailId,
|
|
338
|
+
transaction: structuredClone(transactionArg),
|
|
339
|
+
};
|
|
340
|
+
await Promise.allSettled(
|
|
341
|
+
[...this.smtpTransactionListeners].map(async (listener) => await listener(event)),
|
|
342
|
+
);
|
|
343
|
+
}).catch((error) => {
|
|
344
|
+
logger.log('warn', `Unable to persist SMTP transaction: ${(error as Error).message}`);
|
|
278
345
|
});
|
|
346
|
+
this.trackUpdatePromise(updatePromise);
|
|
347
|
+
return updatePromise;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Subscribe to updates only after the SMTP transaction is durably stored. */
|
|
351
|
+
public onSmtpTransactionPersisted(
|
|
352
|
+
listenerArg: (eventArg: ISmtpTransactionPersistedEvent) => void | Promise<void>,
|
|
353
|
+
): () => void {
|
|
354
|
+
this.smtpTransactionListeners.add(listenerArg);
|
|
355
|
+
return () => this.smtpTransactionListeners.delete(listenerArg);
|
|
356
|
+
}
|
|
357
|
+
/** Subscribe to queue state changes only after the CachedEmail row is durable. */
|
|
358
|
+
public onEmailQueuePersisted(
|
|
359
|
+
listenerArg: (eventArg: IEmailQueuePersistedEvent) => void | Promise<void>,
|
|
360
|
+
): () => void {
|
|
361
|
+
this.emailQueuePersistedListeners.add(listenerArg);
|
|
362
|
+
return () => this.emailQueuePersistedListeners.delete(listenerArg);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private async notifyEmailQueuePersisted(
|
|
366
|
+
cachedEmailArg: CachedEmail,
|
|
367
|
+
reasonArg: string,
|
|
368
|
+
): Promise<void> {
|
|
369
|
+
if (this.emailQueuePersistedListeners.size === 0) return;
|
|
370
|
+
const event: IEmailQueuePersistedEvent = {
|
|
371
|
+
cachedEmailId: cachedEmailArg.id,
|
|
372
|
+
status: cachedEmailArg.status,
|
|
373
|
+
reason: reasonArg,
|
|
374
|
+
};
|
|
375
|
+
await Promise.allSettled(
|
|
376
|
+
[...this.emailQueuePersistedListeners].map(async (listener) => await listener(event)),
|
|
377
|
+
);
|
|
279
378
|
}
|
|
280
379
|
|
|
281
380
|
public async drainQueueUpdates(): Promise<void> {
|
|
@@ -295,6 +394,22 @@ export class AcceptedEmailSpool {
|
|
|
295
394
|
}
|
|
296
395
|
}
|
|
297
396
|
|
|
397
|
+
/** Reconcile persisted queue traces after restart before queue recovery runs. */
|
|
398
|
+
public async recoverSmtpTransactionHistory(emailServerArg: UnifiedEmailServer): Promise<void> {
|
|
399
|
+
for (const item of (emailServerArg as TSmartMtaQueueReader).getQueueItems?.() || []) {
|
|
400
|
+
const cachedEmailId = this.getCachedEmailIdFromQueueItem(item);
|
|
401
|
+
if (!cachedEmailId || !item.smtpTransactions?.length) continue;
|
|
402
|
+
await this.runCachedEmailUpdate(cachedEmailId, async () => {
|
|
403
|
+
const cachedEmail = await CachedEmail.findById(cachedEmailId);
|
|
404
|
+
if (!cachedEmail) return;
|
|
405
|
+
for (const transaction of item.smtpTransactions || []) {
|
|
406
|
+
this.appendSmtpTransaction(cachedEmail, transaction);
|
|
407
|
+
}
|
|
408
|
+
await cachedEmail.save();
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
298
413
|
/** Requeue emails left in 'queued' state by a previous process as pending. */
|
|
299
414
|
public async recoverQueuedEmails(): Promise<void> {
|
|
300
415
|
while (true) {
|
|
@@ -309,6 +424,7 @@ export class AcceptedEmailSpool {
|
|
|
309
424
|
queuedEmail.status = 'pending';
|
|
310
425
|
queuedEmail.nextAttempt = new Date();
|
|
311
426
|
await queuedEmail.save();
|
|
427
|
+
await this.notifyEmailQueuePersisted(queuedEmail, 'email-queue-recovered');
|
|
312
428
|
}
|
|
313
429
|
if (queuedEmails.length < ACCEPTED_EMAIL_SPOOL_BATCH_SIZE) {
|
|
314
430
|
return;
|
|
@@ -332,7 +448,7 @@ export class AcceptedEmailSpool {
|
|
|
332
448
|
continue;
|
|
333
449
|
}
|
|
334
450
|
const session = this.buildCachedEmailSession(cachedEmail);
|
|
335
|
-
const rawMessage =
|
|
451
|
+
const rawMessage = await this.readRawMessage(cachedEmail);
|
|
336
452
|
await this.processAcceptedCachedEmail(cachedEmail, rawMessage, session, emailServer);
|
|
337
453
|
}
|
|
338
454
|
} finally {
|
|
@@ -349,6 +465,7 @@ export class AcceptedEmailSpool {
|
|
|
349
465
|
cachedEmail.status = 'processing';
|
|
350
466
|
cachedEmail.nextAttempt = new Date(Date.now() + ACCEPTED_EMAIL_QUEUE_LEASE_MS);
|
|
351
467
|
await cachedEmail.save();
|
|
468
|
+
await this.notifyEmailQueuePersisted(cachedEmail, 'email-processing');
|
|
352
469
|
|
|
353
470
|
try {
|
|
354
471
|
if (plugins.buffer.Buffer.isBuffer(emailData)) {
|
|
@@ -360,13 +477,18 @@ export class AcceptedEmailSpool {
|
|
|
360
477
|
if (handledByTypedEndpoint) {
|
|
361
478
|
cachedEmail.markDelivered();
|
|
362
479
|
await cachedEmail.save();
|
|
480
|
+
await this.notifyEmailQueuePersisted(cachedEmail, 'email-delivered-to-typed-endpoint');
|
|
363
481
|
return;
|
|
364
482
|
}
|
|
365
483
|
}
|
|
366
484
|
|
|
367
485
|
await emailServer.processEmailByMode(emailData, session);
|
|
368
486
|
if (session.matchedRoute?.action.type === 'forward') {
|
|
369
|
-
cachedEmail.
|
|
487
|
+
const currentCachedEmail = await CachedEmail.findById(cachedEmail.id) || cachedEmail;
|
|
488
|
+
currentCachedEmail.markDelivered();
|
|
489
|
+
await currentCachedEmail.save();
|
|
490
|
+
await this.notifyEmailQueuePersisted(currentCachedEmail, 'email-forwarded');
|
|
491
|
+
return;
|
|
370
492
|
} else {
|
|
371
493
|
const currentCachedEmail = await CachedEmail.findById(cachedEmail.id) || cachedEmail;
|
|
372
494
|
if (this.isCachedEmailTerminal(currentCachedEmail) || currentCachedEmail.status === 'deferred') {
|
|
@@ -375,13 +497,14 @@ export class AcceptedEmailSpool {
|
|
|
375
497
|
currentCachedEmail.status = 'queued';
|
|
376
498
|
currentCachedEmail.nextAttempt = new Date(Date.now() + ACCEPTED_EMAIL_QUEUE_LEASE_MS);
|
|
377
499
|
await currentCachedEmail.save();
|
|
500
|
+
await this.notifyEmailQueuePersisted(currentCachedEmail, 'email-queued');
|
|
378
501
|
return;
|
|
379
502
|
}
|
|
380
|
-
await cachedEmail.save();
|
|
381
503
|
} catch (error: unknown) {
|
|
382
504
|
cachedEmail.scheduleRetry(ACCEPTED_EMAIL_RETRY_DELAY_MS);
|
|
383
505
|
cachedEmail.lastError = (error as Error).message;
|
|
384
506
|
await cachedEmail.save();
|
|
507
|
+
await this.notifyEmailQueuePersisted(cachedEmail, 'email-spool-deferred');
|
|
385
508
|
logger.log('warn', `Accepted email ${cachedEmail.id} deferred after SmartMTA handoff failure: ${(error as Error).message}`);
|
|
386
509
|
}
|
|
387
510
|
}
|
|
@@ -405,7 +528,7 @@ export class AcceptedEmailSpool {
|
|
|
405
528
|
clientHostname: storedSession.clientHostname || '',
|
|
406
529
|
mailFrom: mailFrom.address,
|
|
407
530
|
rcptTo: rcptTo.map((recipient) => recipient.address),
|
|
408
|
-
emailData:
|
|
531
|
+
emailData: '',
|
|
409
532
|
useTLS: !!storedSession.secure,
|
|
410
533
|
connectionEnded: false,
|
|
411
534
|
remoteAddress: storedSession.remoteAddress || '127.0.0.1',
|
|
@@ -456,6 +579,9 @@ export class AcceptedEmailSpool {
|
|
|
456
579
|
return;
|
|
457
580
|
}
|
|
458
581
|
cachedEmail.attempts = Math.max(cachedEmail.attempts || 0, item.attempts || 0);
|
|
582
|
+
for (const transaction of item.smtpTransactions || []) {
|
|
583
|
+
this.appendSmtpTransaction(cachedEmail, transaction);
|
|
584
|
+
}
|
|
459
585
|
this.updateSmartMtaRouteData(cachedEmail, item, status);
|
|
460
586
|
if (status === 'delivered') {
|
|
461
587
|
cachedEmail.markDelivered();
|
|
@@ -471,6 +597,7 @@ export class AcceptedEmailSpool {
|
|
|
471
597
|
cachedEmail.nextAttempt = new Date(Date.now() + ACCEPTED_EMAIL_QUEUE_LEASE_MS);
|
|
472
598
|
}
|
|
473
599
|
await cachedEmail.save();
|
|
600
|
+
await this.notifyEmailQueuePersisted(cachedEmail, `email-queue-${status}`);
|
|
474
601
|
}
|
|
475
602
|
|
|
476
603
|
private async waitForPromiseToSettleWithTimeout(
|
|
@@ -538,10 +665,14 @@ export class AcceptedEmailSpool {
|
|
|
538
665
|
if (queueItem.lastError) {
|
|
539
666
|
cachedEmail.lastError = queueItem.lastError;
|
|
540
667
|
}
|
|
668
|
+
for (const transaction of queueItem.smtpTransactions || []) {
|
|
669
|
+
this.appendSmtpTransaction(cachedEmail, transaction);
|
|
670
|
+
}
|
|
541
671
|
this.updateSmartMtaRouteData(cachedEmail, queueItem, status);
|
|
542
672
|
const smartMtaNextAttempt = queueItem.nextAttempt || new Date(Date.now() + ACCEPTED_EMAIL_RETRY_DELAY_MS);
|
|
543
673
|
cachedEmail.nextAttempt = new Date(smartMtaNextAttempt.getTime() + ACCEPTED_EMAIL_QUEUE_LEASE_MS);
|
|
544
674
|
await cachedEmail.save();
|
|
675
|
+
await this.notifyEmailQueuePersisted(cachedEmail, 'email-live-queue-postponed');
|
|
545
676
|
return true;
|
|
546
677
|
}
|
|
547
678
|
|
|
@@ -577,6 +708,81 @@ export class AcceptedEmailSpool {
|
|
|
577
708
|
cachedEmail.routeData = JSON.stringify(routeData);
|
|
578
709
|
}
|
|
579
710
|
|
|
711
|
+
public async readRawMessage(cachedEmailArg: CachedEmail): Promise<plugins.buffer.Buffer> {
|
|
712
|
+
if (cachedEmailArg.rawContentObjectKey) {
|
|
713
|
+
const blobStorage = this.dcRouterRef.getSmartMtaBlobStorageManager();
|
|
714
|
+
if (!blobStorage) {
|
|
715
|
+
throw new Error(`SmartBucket storage is unavailable for accepted email ${cachedEmailArg.id}`);
|
|
716
|
+
}
|
|
717
|
+
const rawMessage = await blobStorage.get(cachedEmailArg.rawContentObjectKey);
|
|
718
|
+
if (!rawMessage) {
|
|
719
|
+
throw new Error(`Raw RFC822 object is missing for accepted email ${cachedEmailArg.id}`);
|
|
720
|
+
}
|
|
721
|
+
return plugins.buffer.Buffer.from(rawMessage);
|
|
722
|
+
}
|
|
723
|
+
throw new Error(`Accepted email ${cachedEmailArg.id} has no raw RFC822 payload`);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
public async deleteRawMessage(cachedEmailArg: CachedEmail): Promise<void> {
|
|
727
|
+
if (!cachedEmailArg.rawContentObjectKey) return;
|
|
728
|
+
const blobStorage = this.dcRouterRef.getSmartMtaBlobStorageManager();
|
|
729
|
+
if (!blobStorage) {
|
|
730
|
+
throw new Error(`SmartBucket storage is unavailable for accepted email ${cachedEmailArg.id}`);
|
|
731
|
+
}
|
|
732
|
+
await blobStorage.delete(cachedEmailArg.rawContentObjectKey);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
private async persistRawMessage(cachedEmailArg: CachedEmail, rawMessageArg: string): Promise<void> {
|
|
736
|
+
const blobStorage = this.dcRouterRef.getSmartMtaBlobStorageManager();
|
|
737
|
+
if (!blobStorage) {
|
|
738
|
+
throw new Error('SmartBucket storage is required before accepting SMTP messages');
|
|
739
|
+
}
|
|
740
|
+
const objectKey = `/email/messages/${cachedEmailArg.id}.eml`;
|
|
741
|
+
const rawMessage = plugins.buffer.Buffer.from(rawMessageArg, 'utf8');
|
|
742
|
+
await blobStorage.set(objectKey, rawMessage);
|
|
743
|
+
cachedEmailArg.rawContent = undefined;
|
|
744
|
+
cachedEmailArg.rawContentObjectKey = objectKey;
|
|
745
|
+
cachedEmailArg.rawContentSize = rawMessage.length;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
private runCachedEmailUpdate(
|
|
749
|
+
cachedEmailIdArg: string | undefined,
|
|
750
|
+
operationArg: () => Promise<void>,
|
|
751
|
+
): Promise<void> {
|
|
752
|
+
if (!cachedEmailIdArg) return operationArg();
|
|
753
|
+
const previous = this.cachedEmailUpdateChains.get(cachedEmailIdArg) || Promise.resolve();
|
|
754
|
+
const current = previous.catch(() => undefined).then(operationArg);
|
|
755
|
+
this.cachedEmailUpdateChains.set(cachedEmailIdArg, current);
|
|
756
|
+
const cleanup = () => {
|
|
757
|
+
if (this.cachedEmailUpdateChains.get(cachedEmailIdArg) === current) {
|
|
758
|
+
this.cachedEmailUpdateChains.delete(cachedEmailIdArg);
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
void current.then(cleanup, cleanup);
|
|
762
|
+
return current;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
private trackUpdatePromise(updatePromiseArg: Promise<void>): void {
|
|
766
|
+
this.queueUpdatePromises.add(updatePromiseArg);
|
|
767
|
+
const cleanup = () => this.queueUpdatePromises.delete(updatePromiseArg);
|
|
768
|
+
void updatePromiseArg.then(cleanup, cleanup);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
private appendSmtpTransaction(
|
|
772
|
+
cachedEmailArg: CachedEmail,
|
|
773
|
+
transactionArg: ICachedEmailSmtpTransaction,
|
|
774
|
+
): void {
|
|
775
|
+
if (typeof cachedEmailArg.appendSmtpTransaction === 'function') {
|
|
776
|
+
cachedEmailArg.appendSmtpTransaction(transactionArg);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
const transactions = (cachedEmailArg.smtpTransactions || []).filter(
|
|
780
|
+
(transaction) => transaction.id !== transactionArg.id,
|
|
781
|
+
);
|
|
782
|
+
transactions.push(structuredClone(transactionArg));
|
|
783
|
+
cachedEmailArg.smtpTransactions = transactions.slice(-20);
|
|
784
|
+
}
|
|
785
|
+
|
|
580
786
|
private removeHeader(headers: Record<string, string>, headerName: string): void {
|
|
581
787
|
const normalizedHeaderName = headerName.toLowerCase();
|
|
582
788
|
for (const key of Object.keys(headers)) {
|
|
@@ -26,6 +26,11 @@ export interface IEmailDomainManagerActionResult {
|
|
|
26
26
|
provisioned?: number;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
export interface IManagedDkimRepairResult {
|
|
30
|
+
repairedDomainIds: string[];
|
|
31
|
+
failedDomainIds: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
29
34
|
/**
|
|
30
35
|
* Persists managed email domains and projects them into SmartMTA. DNS mutation
|
|
31
36
|
* is delegated exclusively to MailDnsSync.
|
|
@@ -340,13 +345,23 @@ export class EmailDomainManager {
|
|
|
340
345
|
}> {
|
|
341
346
|
const doc = await EmailDomainDoc.findByDomain(domainName.toLowerCase());
|
|
342
347
|
if (!doc) return { ready: true, edgeIds: [] }; // unmanaged/static domain policy
|
|
348
|
+
const repairFailure = doc.reconciliation?.errors?.find(
|
|
349
|
+
(error) => error.code === 'DKIM_KEY_REPAIR_FAILED',
|
|
350
|
+
);
|
|
351
|
+
if (repairFailure) {
|
|
352
|
+
return { ready: false, reason: repairFailure.message, edgeIds: [] };
|
|
353
|
+
}
|
|
343
354
|
const activeDkim = this.getActiveDkim(doc);
|
|
344
355
|
if (!activeDkim) return { ready: false, reason: 'no usable active DKIM key', edgeIds: [] };
|
|
345
356
|
if (activeDkim.selector !== 'default' && !this.selectorCorrectSigningCapability) {
|
|
346
357
|
return { ready: false, reason: 'SmartMTA selector-correct signing capability is unavailable', edgeIds: [] };
|
|
347
358
|
}
|
|
348
|
-
|
|
349
|
-
|
|
359
|
+
const reconciliation = doc.reconciliation;
|
|
360
|
+
if (
|
|
361
|
+
!reconciliation?.activeRevision
|
|
362
|
+
|| reconciliation.activeRevision.generation !== reconciliation.desiredGeneration
|
|
363
|
+
) {
|
|
364
|
+
return { ready: false, reason: 'no current validated active DNS revision', edgeIds: [] };
|
|
350
365
|
}
|
|
351
366
|
const live = await this.dcRouter.mailEdgeEligibility.resolveActiveForDomain(doc);
|
|
352
367
|
if (live.edges.length === 0) {
|
|
@@ -359,6 +374,123 @@ export class EmailDomainManager {
|
|
|
359
374
|
};
|
|
360
375
|
}
|
|
361
376
|
|
|
377
|
+
/**
|
|
378
|
+
* Caller-owned repair for legacy managed domains. SmartMTA remains in
|
|
379
|
+
* validation-only mode; dcrouter explicitly owns any required key creation.
|
|
380
|
+
*/
|
|
381
|
+
public async repairManagedDkimBeforeEmailStart(): Promise<IManagedDkimRepairResult> {
|
|
382
|
+
const repairedDomainIds: string[] = [];
|
|
383
|
+
const failedDomainIds: string[] = [];
|
|
384
|
+
|
|
385
|
+
await this.runMutationExclusive(async () => {
|
|
386
|
+
const dkimCreator = this.dkimCreator;
|
|
387
|
+
if (!dkimCreator) {
|
|
388
|
+
throw new Error('DKIM key creator is not ready for managed-domain repair');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
for (const doc of await EmailDomainDoc.findAll()) {
|
|
392
|
+
if (doc.reconciliation?.lifecycleStatus === 'deleting') continue;
|
|
393
|
+
const currentActive = this.getActiveDkim(doc);
|
|
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
|
+
}
|
|
470
|
+
|
|
471
|
+
// Persistence failures are global prerequisites, not per-domain key
|
|
472
|
+
// failures. Keep this save outside the material-repair catch.
|
|
473
|
+
await doc.save();
|
|
474
|
+
if (outcome === 'repaired') {
|
|
475
|
+
repairedDomainIds.push(doc.id);
|
|
476
|
+
} else if (outcome === 'failed') {
|
|
477
|
+
failedDomainIds.push(doc.id);
|
|
478
|
+
logger.log('error', failureMessage!);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
await this.syncManagedDomainsToRuntime({ excludeDkimRepairFailures: true });
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
if (repairedDomainIds.length > 0) {
|
|
486
|
+
this.dcRouter.mailDnsSync?.requestSync(
|
|
487
|
+
'caller-managed DKIM material repaired before SmartMTA start',
|
|
488
|
+
[...new Set(repairedDomainIds)],
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
return { repairedDomainIds, failedDomainIds };
|
|
492
|
+
}
|
|
493
|
+
|
|
362
494
|
public async requestDkimRotation(id: string): Promise<IEmailDomainManagerActionResult> {
|
|
363
495
|
const mutation = await this.runMutationExclusive(async () => {
|
|
364
496
|
const doc = await EmailDomainDoc.findById(id);
|
|
@@ -472,6 +604,45 @@ export class EmailDomainManager {
|
|
|
472
604
|
};
|
|
473
605
|
}
|
|
474
606
|
|
|
607
|
+
private invalidateDkimReconciliation(
|
|
608
|
+
doc: EmailDomainDoc,
|
|
609
|
+
code: string,
|
|
610
|
+
message: string,
|
|
611
|
+
retryable = true,
|
|
612
|
+
) {
|
|
613
|
+
const previous = doc.reconciliation || {
|
|
614
|
+
lifecycleStatus: 'pending' as const,
|
|
615
|
+
desiredGeneration: 0,
|
|
616
|
+
intents: [],
|
|
617
|
+
errors: [],
|
|
618
|
+
};
|
|
619
|
+
const {
|
|
620
|
+
activeRevision: _activeRevision,
|
|
621
|
+
pendingRevision: _pendingRevision,
|
|
622
|
+
intents: _intents,
|
|
623
|
+
errors: _errors,
|
|
624
|
+
lastSuccessAt: _lastSuccessAt,
|
|
625
|
+
retryAt: _retryAt,
|
|
626
|
+
...preserved
|
|
627
|
+
} = previous;
|
|
628
|
+
const alreadyInvalidated = (
|
|
629
|
+
!previous.activeRevision
|
|
630
|
+
&& previous.errors.length === 1
|
|
631
|
+
&& previous.errors[0].code === code
|
|
632
|
+
&& previous.errors[0].message === message
|
|
633
|
+
);
|
|
634
|
+
return {
|
|
635
|
+
...preserved,
|
|
636
|
+
lifecycleStatus: retryable ? 'pending' as const : 'failed' as const,
|
|
637
|
+
desiredGeneration: alreadyInvalidated
|
|
638
|
+
? previous.desiredGeneration
|
|
639
|
+
: previous.desiredGeneration + 1,
|
|
640
|
+
intents: [],
|
|
641
|
+
errors: [{ code, message, retryable }],
|
|
642
|
+
...(retryable ? { retryAt: new Date().toISOString() } : {}),
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
|
|
475
646
|
private capabilityError(code: string, message: string): Error {
|
|
476
647
|
const error = new Error(message) as Error & Record<string, unknown>;
|
|
477
648
|
error.code = code;
|
|
@@ -545,17 +716,23 @@ export class EmailDomainManager {
|
|
|
545
716
|
.sort((left, right) => right.name.length - left.name.length)[0] || null;
|
|
546
717
|
}
|
|
547
718
|
|
|
548
|
-
private async buildManagedDomainConfigs(
|
|
719
|
+
private async buildManagedDomainConfigs(optionsArg: {
|
|
720
|
+
excludeDkimRepairFailures?: boolean;
|
|
721
|
+
} = {}): Promise<IEmailDomainConfig[]> {
|
|
549
722
|
const docs = await EmailDomainDoc.findAll();
|
|
550
723
|
const managedConfigs: IEmailDomainConfig[] = [];
|
|
551
724
|
for (const doc of docs) {
|
|
552
725
|
if (doc.reconciliation?.lifecycleStatus === 'deleting') continue;
|
|
726
|
+
const dkimRepairFailed = this.hasDkimRepairFailure(doc);
|
|
727
|
+
if (dkimRepairFailed && optionsArg.excludeDkimRepairFailures) continue;
|
|
553
728
|
if (!await DomainDoc.findById(doc.linkedDomainId)) {
|
|
554
729
|
logger.log('warn', `Managed email domain ${doc.domain} has no linked DNS domain; keeping it out of runtime`);
|
|
555
730
|
continue;
|
|
556
731
|
}
|
|
557
732
|
const activeDkim = this.getActiveDkim(doc);
|
|
558
|
-
const dkimConfig =
|
|
733
|
+
const dkimConfig = !dkimRepairFailed
|
|
734
|
+
&& activeDkim
|
|
735
|
+
&& (activeDkim.selector === 'default' || this.selectorCorrectSigningCapability)
|
|
559
736
|
? {
|
|
560
737
|
selector: activeDkim.selector,
|
|
561
738
|
keySize: activeDkim.keySize,
|
|
@@ -574,13 +751,24 @@ export class EmailDomainManager {
|
|
|
574
751
|
return managedConfigs;
|
|
575
752
|
}
|
|
576
753
|
|
|
577
|
-
public async syncManagedDomainsToRuntime(
|
|
754
|
+
public async syncManagedDomainsToRuntime(optionsArg: {
|
|
755
|
+
excludeDkimRepairFailures?: boolean;
|
|
756
|
+
} = {}): Promise<void> {
|
|
578
757
|
if (!this.dcRouter.options?.emailConfig) return;
|
|
758
|
+
const failedDomainNames = new Set(
|
|
759
|
+
(await EmailDomainDoc.findAll())
|
|
760
|
+
.filter((doc) => this.hasDkimRepairFailure(doc))
|
|
761
|
+
.map((doc) => doc.domain.toLowerCase()),
|
|
762
|
+
);
|
|
579
763
|
const mergedDomains = new Map<string, IEmailDomainConfig>();
|
|
580
764
|
for (const domainConfig of this.baseEmailDomains) {
|
|
581
|
-
|
|
765
|
+
const key = domainConfig.domain.toLowerCase();
|
|
766
|
+
if (failedDomainNames.has(key) && optionsArg.excludeDkimRepairFailures) continue;
|
|
767
|
+
const projectedConfig = JSON.parse(JSON.stringify(domainConfig)) as IEmailDomainConfig;
|
|
768
|
+
if (failedDomainNames.has(key)) delete projectedConfig.dkim;
|
|
769
|
+
mergedDomains.set(key, projectedConfig);
|
|
582
770
|
}
|
|
583
|
-
for (const managedConfig of await this.buildManagedDomainConfigs()) {
|
|
771
|
+
for (const managedConfig of await this.buildManagedDomainConfigs(optionsArg)) {
|
|
584
772
|
const key = managedConfig.domain.toLowerCase();
|
|
585
773
|
if (mergedDomains.has(key)) {
|
|
586
774
|
logger.log('warn', `Managed email domain ${managedConfig.domain} duplicates a configured domain; keeping the configured definition`);
|
|
@@ -592,4 +780,10 @@ export class EmailDomainManager {
|
|
|
592
780
|
this.dcRouter.options.emailConfig.domains = domains;
|
|
593
781
|
this.dcRouter.emailServer?.updateOptions({ domains });
|
|
594
782
|
}
|
|
783
|
+
|
|
784
|
+
private hasDkimRepairFailure(doc: EmailDomainDoc): boolean {
|
|
785
|
+
return (doc.reconciliation?.errors || []).some(
|
|
786
|
+
(error) => error.code === 'DKIM_KEY_REPAIR_FAILED',
|
|
787
|
+
);
|
|
788
|
+
}
|
|
595
789
|
}
|
|
@@ -155,7 +155,7 @@ export class EmailSettingsManager {
|
|
|
155
155
|
const nextConfig: IUnifiedEmailServerOptions | undefined = clonePlain(existingConfig) || (nextEnabled ? {
|
|
156
156
|
hostname: 'localhost',
|
|
157
157
|
ports: [...defaultEmailPorts],
|
|
158
|
-
queue: { storageMode: '
|
|
158
|
+
queue: { storageMode: 'managed' },
|
|
159
159
|
domains: [],
|
|
160
160
|
routes: [],
|
|
161
161
|
} : undefined);
|