@serve.zone/dcrouter 13.17.9 → 13.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_serve/bundle.js +1 -1
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +7 -5
- package/dist_ts/classes.dcrouter.js +104 -83
- package/dist_ts/email/classes.email-domain.manager.d.ts +7 -0
- package/dist_ts/email/classes.email-domain.manager.js +111 -29
- package/dist_ts/email/classes.smartmta-storage-manager.d.ts +13 -0
- package/dist_ts/email/classes.smartmta-storage-manager.js +101 -0
- package/dist_ts/email/index.d.ts +1 -0
- package/dist_ts/email/index.js +2 -1
- package/dist_ts/opsserver/handlers/email-ops.handler.js +6 -15
- package/dist_ts/opsserver/handlers/stats.handler.js +41 -7
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/package.json +4 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +130 -92
- package/ts/email/classes.email-domain.manager.ts +129 -24
- package/ts/email/classes.smartmta-storage-manager.ts +108 -0
- package/ts/email/index.ts +1 -0
- package/ts/opsserver/handlers/email-ops.handler.ts +5 -19
- package/ts/opsserver/handlers/stats.handler.ts +43 -7
- package/ts_web/00_commitinfo_data.ts +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as plugins from '../plugins.js';
|
|
2
|
+
import type { IEmailDomainConfig } from '@push.rocks/smartmta';
|
|
2
3
|
import { logger } from '../logger.js';
|
|
3
4
|
import { EmailDomainDoc } from '../db/documents/classes.email-domain.doc.js';
|
|
4
5
|
import { DomainDoc } from '../db/documents/classes.domain.doc.js';
|
|
@@ -15,9 +16,12 @@ import type { IEmailDomain, IEmailDnsRecord, TDnsRecordStatus } from '../../ts_i
|
|
|
15
16
|
*/
|
|
16
17
|
export class EmailDomainManager {
|
|
17
18
|
private dcRouter: any; // DcRouter — avoids circular import
|
|
19
|
+
private readonly baseEmailDomains: IEmailDomainConfig[];
|
|
18
20
|
|
|
19
21
|
constructor(dcRouterRef: any) {
|
|
20
22
|
this.dcRouter = dcRouterRef;
|
|
23
|
+
this.baseEmailDomains = ((this.dcRouter.options?.emailConfig?.domains || []) as IEmailDomainConfig[])
|
|
24
|
+
.map((domainConfig) => JSON.parse(JSON.stringify(domainConfig)) as IEmailDomainConfig);
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
private get dnsManager(): DnsManager | undefined {
|
|
@@ -32,6 +36,12 @@ export class EmailDomainManager {
|
|
|
32
36
|
return this.dcRouter.options?.emailConfig?.hostname || this.dcRouter.options?.tls?.domain || 'localhost';
|
|
33
37
|
}
|
|
34
38
|
|
|
39
|
+
public async start(): Promise<void> {
|
|
40
|
+
await this.syncManagedDomainsToRuntime();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public async stop(): Promise<void> {}
|
|
44
|
+
|
|
35
45
|
// ---------------------------------------------------------------------------
|
|
36
46
|
// CRUD
|
|
37
47
|
// ---------------------------------------------------------------------------
|
|
@@ -64,6 +74,9 @@ export class EmailDomainManager {
|
|
|
64
74
|
const domainName = subdomain ? `${subdomain}.${baseDomain}` : baseDomain;
|
|
65
75
|
|
|
66
76
|
// Check for duplicates
|
|
77
|
+
if (this.isDomainAlreadyConfigured(domainName)) {
|
|
78
|
+
throw new Error(`Email domain already configured for ${domainName}`);
|
|
79
|
+
}
|
|
67
80
|
const existing = await EmailDomainDoc.findByDomain(domainName);
|
|
68
81
|
if (existing) {
|
|
69
82
|
throw new Error(`Email domain already exists for ${domainName}`);
|
|
@@ -77,8 +90,8 @@ export class EmailDomainManager {
|
|
|
77
90
|
let publicKey: string | undefined;
|
|
78
91
|
if (this.dkimCreator) {
|
|
79
92
|
try {
|
|
80
|
-
await this.dkimCreator.
|
|
81
|
-
const dnsRecord = await this.dkimCreator.
|
|
93
|
+
await this.dkimCreator.handleDKIMKeysForSelector(domainName, selector, keySize);
|
|
94
|
+
const dnsRecord = await this.dkimCreator.getDNSRecordForDomain(domainName, selector);
|
|
82
95
|
// Extract public key from the DNS record value
|
|
83
96
|
const match = dnsRecord?.value?.match(/p=([A-Za-z0-9+/=]+)/);
|
|
84
97
|
publicKey = match ? match[1] : undefined;
|
|
@@ -110,6 +123,7 @@ export class EmailDomainManager {
|
|
|
110
123
|
doc.createdAt = now;
|
|
111
124
|
doc.updatedAt = now;
|
|
112
125
|
await doc.save();
|
|
126
|
+
await this.syncManagedDomainsToRuntime();
|
|
113
127
|
|
|
114
128
|
logger.log('info', `Email domain created: ${domainName}`);
|
|
115
129
|
return this.docToInterface(doc);
|
|
@@ -131,12 +145,14 @@ export class EmailDomainManager {
|
|
|
131
145
|
if (changes.rateLimits !== undefined) doc.rateLimits = changes.rateLimits;
|
|
132
146
|
doc.updatedAt = new Date().toISOString();
|
|
133
147
|
await doc.save();
|
|
148
|
+
await this.syncManagedDomainsToRuntime();
|
|
134
149
|
}
|
|
135
150
|
|
|
136
151
|
public async deleteEmailDomain(id: string): Promise<void> {
|
|
137
152
|
const doc = await EmailDomainDoc.findById(id);
|
|
138
153
|
if (!doc) throw new Error(`Email domain not found: ${id}`);
|
|
139
154
|
await doc.delete();
|
|
155
|
+
await this.syncManagedDomainsToRuntime();
|
|
140
156
|
logger.log('info', `Email domain deleted: ${doc.domain}`);
|
|
141
157
|
}
|
|
142
158
|
|
|
@@ -153,8 +169,17 @@ export class EmailDomainManager {
|
|
|
153
169
|
|
|
154
170
|
const domain = doc.domain;
|
|
155
171
|
const selector = doc.dkim.selector;
|
|
156
|
-
const publicKey = doc.dkim.publicKey || '';
|
|
157
172
|
const hostname = this.emailHostname;
|
|
173
|
+
let dkimValue = `v=DKIM1; h=sha256; k=rsa; p=${doc.dkim.publicKey || ''}`;
|
|
174
|
+
|
|
175
|
+
if (this.dkimCreator) {
|
|
176
|
+
try {
|
|
177
|
+
const dnsRecord = await this.dkimCreator.getDNSRecordForDomain(domain, selector);
|
|
178
|
+
dkimValue = dnsRecord.value;
|
|
179
|
+
} catch (err: unknown) {
|
|
180
|
+
logger.log('warn', `Failed to load DKIM DNS record for ${domain}: ${(err as Error).message}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
158
183
|
|
|
159
184
|
const records: IEmailDnsRecord[] = [
|
|
160
185
|
{
|
|
@@ -172,7 +197,7 @@ export class EmailDomainManager {
|
|
|
172
197
|
{
|
|
173
198
|
type: 'TXT',
|
|
174
199
|
name: `${selector}._domainkey.${domain}`,
|
|
175
|
-
value:
|
|
200
|
+
value: dkimValue,
|
|
176
201
|
status: doc.dnsStatus.dkim,
|
|
177
202
|
},
|
|
178
203
|
{
|
|
@@ -207,17 +232,7 @@ export class EmailDomainManager {
|
|
|
207
232
|
|
|
208
233
|
for (const required of requiredRecords) {
|
|
209
234
|
// Check if a matching record already exists
|
|
210
|
-
const exists = existingRecords.some((r) =>
|
|
211
|
-
if (required.type === 'MX') {
|
|
212
|
-
return r.type === 'MX' && r.name.toLowerCase() === required.name.toLowerCase();
|
|
213
|
-
}
|
|
214
|
-
// For TXT records, match by name AND check value prefix (v=spf1, v=DKIM1, v=DMARC1)
|
|
215
|
-
if (r.type !== 'TXT' || r.name.toLowerCase() !== required.name.toLowerCase()) return false;
|
|
216
|
-
if (required.value.startsWith('v=spf1')) return r.value.includes('v=spf1');
|
|
217
|
-
if (required.value.startsWith('v=DKIM1')) return r.value.includes('v=DKIM1');
|
|
218
|
-
if (required.value.startsWith('v=DMARC1')) return r.value.includes('v=DMARC1');
|
|
219
|
-
return false;
|
|
220
|
-
});
|
|
235
|
+
const exists = existingRecords.some((r) => this.recordMatchesRequired(r, required));
|
|
221
236
|
|
|
222
237
|
if (!exists) {
|
|
223
238
|
try {
|
|
@@ -259,16 +274,23 @@ export class EmailDomainManager {
|
|
|
259
274
|
const resolver = new plugins.dns.promises.Resolver();
|
|
260
275
|
|
|
261
276
|
// MX check
|
|
262
|
-
|
|
277
|
+
const requiredRecords = await this.getRequiredDnsRecords(id);
|
|
278
|
+
|
|
279
|
+
const mxRecord = requiredRecords.find((record) => record.type === 'MX');
|
|
280
|
+
const spfRecord = requiredRecords.find((record) => record.name === domain && record.value.startsWith('v=spf1'));
|
|
281
|
+
const dkimRecord = requiredRecords.find((record) => record.name === `${selector}._domainkey.${domain}`);
|
|
282
|
+
const dmarcRecord = requiredRecords.find((record) => record.name === `_dmarc.${domain}`);
|
|
283
|
+
|
|
284
|
+
doc.dnsStatus.mx = await this.checkMx(resolver, domain, mxRecord?.value);
|
|
263
285
|
|
|
264
286
|
// SPF check
|
|
265
|
-
doc.dnsStatus.spf = await this.checkTxtRecord(resolver, domain,
|
|
287
|
+
doc.dnsStatus.spf = await this.checkTxtRecord(resolver, domain, spfRecord?.value);
|
|
266
288
|
|
|
267
289
|
// DKIM check
|
|
268
|
-
doc.dnsStatus.dkim = await this.checkTxtRecord(resolver, `${selector}._domainkey.${domain}`,
|
|
290
|
+
doc.dnsStatus.dkim = await this.checkTxtRecord(resolver, `${selector}._domainkey.${domain}`, dkimRecord?.value);
|
|
269
291
|
|
|
270
292
|
// DMARC check
|
|
271
|
-
doc.dnsStatus.dmarc = await this.checkTxtRecord(resolver, `_dmarc.${domain}`,
|
|
293
|
+
doc.dnsStatus.dmarc = await this.checkTxtRecord(resolver, `_dmarc.${domain}`, dmarcRecord?.value);
|
|
272
294
|
|
|
273
295
|
doc.dnsStatus.lastCheckedAt = new Date().toISOString();
|
|
274
296
|
doc.updatedAt = new Date().toISOString();
|
|
@@ -277,10 +299,28 @@ export class EmailDomainManager {
|
|
|
277
299
|
return this.getRequiredDnsRecords(id);
|
|
278
300
|
}
|
|
279
301
|
|
|
280
|
-
private
|
|
302
|
+
private recordMatchesRequired(record: DnsRecordDoc, required: IEmailDnsRecord): boolean {
|
|
303
|
+
if (record.type !== required.type || record.name.toLowerCase() !== required.name.toLowerCase()) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
return record.value.trim() === required.value.trim();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private async checkMx(
|
|
310
|
+
resolver: plugins.dns.promises.Resolver,
|
|
311
|
+
domain: string,
|
|
312
|
+
expectedValue?: string,
|
|
313
|
+
): Promise<TDnsRecordStatus> {
|
|
281
314
|
try {
|
|
282
315
|
const records = await resolver.resolveMx(domain);
|
|
283
|
-
|
|
316
|
+
if (!records || records.length === 0) {
|
|
317
|
+
return 'missing';
|
|
318
|
+
}
|
|
319
|
+
if (!expectedValue) {
|
|
320
|
+
return 'valid';
|
|
321
|
+
}
|
|
322
|
+
const found = records.some((record) => `${record.priority} ${record.exchange}`.trim() === expectedValue.trim());
|
|
323
|
+
return found ? 'valid' : 'invalid';
|
|
284
324
|
} catch {
|
|
285
325
|
return 'missing';
|
|
286
326
|
}
|
|
@@ -289,13 +329,19 @@ export class EmailDomainManager {
|
|
|
289
329
|
private async checkTxtRecord(
|
|
290
330
|
resolver: plugins.dns.promises.Resolver,
|
|
291
331
|
name: string,
|
|
292
|
-
|
|
332
|
+
expectedValue?: string,
|
|
293
333
|
): Promise<TDnsRecordStatus> {
|
|
294
334
|
try {
|
|
295
335
|
const records = await resolver.resolveTxt(name);
|
|
296
336
|
const flat = records.map((r) => r.join(''));
|
|
297
|
-
|
|
298
|
-
|
|
337
|
+
if (flat.length === 0) {
|
|
338
|
+
return 'missing';
|
|
339
|
+
}
|
|
340
|
+
if (!expectedValue) {
|
|
341
|
+
return 'valid';
|
|
342
|
+
}
|
|
343
|
+
const found = flat.some((record) => record.trim() === expectedValue.trim());
|
|
344
|
+
return found ? 'valid' : 'invalid';
|
|
299
345
|
} catch {
|
|
300
346
|
return 'missing';
|
|
301
347
|
}
|
|
@@ -318,4 +364,63 @@ export class EmailDomainManager {
|
|
|
318
364
|
updatedAt: doc.updatedAt,
|
|
319
365
|
};
|
|
320
366
|
}
|
|
367
|
+
|
|
368
|
+
private isDomainAlreadyConfigured(domainName: string): boolean {
|
|
369
|
+
const configuredDomains = ((this.dcRouter.options?.emailConfig?.domains || []) as IEmailDomainConfig[])
|
|
370
|
+
.map((domainConfig) => domainConfig.domain.toLowerCase());
|
|
371
|
+
return configuredDomains.includes(domainName.toLowerCase());
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private async buildManagedDomainConfigs(): Promise<IEmailDomainConfig[]> {
|
|
375
|
+
const docs = await EmailDomainDoc.findAll();
|
|
376
|
+
const managedConfigs: IEmailDomainConfig[] = [];
|
|
377
|
+
|
|
378
|
+
for (const doc of docs) {
|
|
379
|
+
const linkedDomain = await DomainDoc.findById(doc.linkedDomainId);
|
|
380
|
+
if (!linkedDomain) {
|
|
381
|
+
logger.log('warn', `Skipping managed email domain ${doc.domain}: linked domain missing`);
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
managedConfigs.push({
|
|
386
|
+
domain: doc.domain,
|
|
387
|
+
dnsMode: linkedDomain.source === 'dcrouter' ? 'internal-dns' : 'external-dns',
|
|
388
|
+
dkim: {
|
|
389
|
+
selector: doc.dkim.selector,
|
|
390
|
+
keySize: doc.dkim.keySize,
|
|
391
|
+
rotateKeys: doc.dkim.rotateKeys,
|
|
392
|
+
rotationInterval: doc.dkim.rotationIntervalDays,
|
|
393
|
+
},
|
|
394
|
+
rateLimits: doc.rateLimits,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return managedConfigs;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private async syncManagedDomainsToRuntime(): Promise<void> {
|
|
402
|
+
if (!this.dcRouter.options?.emailConfig) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const mergedDomains = new Map<string, IEmailDomainConfig>();
|
|
407
|
+
for (const domainConfig of this.baseEmailDomains) {
|
|
408
|
+
mergedDomains.set(domainConfig.domain.toLowerCase(), JSON.parse(JSON.stringify(domainConfig)) as IEmailDomainConfig);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
for (const managedConfig of await this.buildManagedDomainConfigs()) {
|
|
412
|
+
const key = managedConfig.domain.toLowerCase();
|
|
413
|
+
if (mergedDomains.has(key)) {
|
|
414
|
+
logger.log('warn', `Managed email domain ${managedConfig.domain} duplicates a configured domain; keeping the configured definition`);
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
mergedDomains.set(key, managedConfig);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const domains = Array.from(mergedDomains.values());
|
|
421
|
+
this.dcRouter.options.emailConfig.domains = domains;
|
|
422
|
+
if (this.dcRouter.emailServer) {
|
|
423
|
+
this.dcRouter.emailServer.updateOptions({ domains });
|
|
424
|
+
}
|
|
425
|
+
}
|
|
321
426
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import type { IStorageManagerLike } from '@push.rocks/smartmta';
|
|
3
|
+
|
|
4
|
+
export class SmartMtaStorageManager implements IStorageManagerLike {
|
|
5
|
+
private readonly resolvedRootDir: string;
|
|
6
|
+
|
|
7
|
+
constructor(private rootDir: string) {
|
|
8
|
+
this.resolvedRootDir = plugins.path.resolve(rootDir);
|
|
9
|
+
plugins.fsUtils.ensureDirSync(this.resolvedRootDir);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
private normalizeKey(key: string): string {
|
|
13
|
+
return key.replace(/^\/+/, '').replace(/\\/g, '/');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
private resolvePathForKey(key: string): string {
|
|
17
|
+
const normalizedKey = this.normalizeKey(key);
|
|
18
|
+
const resolvedPath = plugins.path.resolve(this.resolvedRootDir, normalizedKey);
|
|
19
|
+
if (
|
|
20
|
+
resolvedPath !== this.resolvedRootDir
|
|
21
|
+
&& !resolvedPath.startsWith(`${this.resolvedRootDir}${plugins.path.sep}`)
|
|
22
|
+
) {
|
|
23
|
+
throw new Error(`Storage key escapes root directory: ${key}`);
|
|
24
|
+
}
|
|
25
|
+
return resolvedPath;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private toStorageKey(filePath: string): string {
|
|
29
|
+
const relativePath = plugins.path.relative(this.resolvedRootDir, filePath).split(plugins.path.sep).join('/');
|
|
30
|
+
return `/${relativePath}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public async get(key: string): Promise<string | null> {
|
|
34
|
+
const filePath = this.resolvePathForKey(key);
|
|
35
|
+
try {
|
|
36
|
+
return await plugins.fs.promises.readFile(filePath, 'utf8');
|
|
37
|
+
} catch (error: unknown) {
|
|
38
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public async set(key: string, value: string): Promise<void> {
|
|
46
|
+
const filePath = this.resolvePathForKey(key);
|
|
47
|
+
await plugins.fs.promises.mkdir(plugins.path.dirname(filePath), { recursive: true });
|
|
48
|
+
await plugins.fs.promises.writeFile(filePath, value, 'utf8');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public async list(prefix: string): Promise<string[]> {
|
|
52
|
+
const prefixPath = this.resolvePathForKey(prefix);
|
|
53
|
+
try {
|
|
54
|
+
const stat = await plugins.fs.promises.stat(prefixPath);
|
|
55
|
+
if (stat.isFile()) {
|
|
56
|
+
return [this.toStorageKey(prefixPath)];
|
|
57
|
+
}
|
|
58
|
+
} catch (error: unknown) {
|
|
59
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const results: string[] = [];
|
|
66
|
+
const walk = async (currentPath: string): Promise<void> => {
|
|
67
|
+
const entries = await plugins.fs.promises.readdir(currentPath, { withFileTypes: true });
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
const entryPath = plugins.path.join(currentPath, entry.name);
|
|
70
|
+
if (entry.isDirectory()) {
|
|
71
|
+
await walk(entryPath);
|
|
72
|
+
} else if (entry.isFile()) {
|
|
73
|
+
results.push(this.toStorageKey(entryPath));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
await walk(prefixPath);
|
|
79
|
+
return results.sort();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public async delete(key: string): Promise<void> {
|
|
83
|
+
const targetPath = this.resolvePathForKey(key);
|
|
84
|
+
try {
|
|
85
|
+
const stat = await plugins.fs.promises.stat(targetPath);
|
|
86
|
+
if (stat.isDirectory()) {
|
|
87
|
+
await plugins.fs.promises.rm(targetPath, { recursive: true, force: true });
|
|
88
|
+
} else {
|
|
89
|
+
await plugins.fs.promises.unlink(targetPath);
|
|
90
|
+
}
|
|
91
|
+
} catch (error: unknown) {
|
|
92
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let currentDir = plugins.path.dirname(targetPath);
|
|
99
|
+
while (currentDir.startsWith(this.resolvedRootDir) && currentDir !== this.resolvedRootDir) {
|
|
100
|
+
const entries = await plugins.fs.promises.readdir(currentDir);
|
|
101
|
+
if (entries.length > 0) {
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
await plugins.fs.promises.rmdir(currentDir);
|
|
105
|
+
currentDir = plugins.path.dirname(currentDir);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
package/ts/email/index.ts
CHANGED
|
@@ -48,7 +48,7 @@ export class EmailOpsHandler {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
const queue = emailServer.deliveryQueue;
|
|
51
|
-
const item =
|
|
51
|
+
const item = emailServer.getQueueItem(dataArg.emailId);
|
|
52
52
|
|
|
53
53
|
if (!item) {
|
|
54
54
|
return { success: false, error: 'Email not found in queue' };
|
|
@@ -82,22 +82,10 @@ export class EmailOpsHandler {
|
|
|
82
82
|
*/
|
|
83
83
|
private getAllQueueEmails(): interfaces.requests.IEmail[] {
|
|
84
84
|
const emailServer = this.opsServerRef.dcRouterRef.emailServer;
|
|
85
|
-
if (!emailServer
|
|
85
|
+
if (!emailServer) {
|
|
86
86
|
return [];
|
|
87
87
|
}
|
|
88
|
-
|
|
89
|
-
const queue = emailServer.deliveryQueue;
|
|
90
|
-
const queueMap = (queue as any).queue as Map<string, any>;
|
|
91
|
-
|
|
92
|
-
if (!queueMap) {
|
|
93
|
-
return [];
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const emails: interfaces.requests.IEmail[] = [];
|
|
97
|
-
|
|
98
|
-
for (const [id, item] of queueMap.entries()) {
|
|
99
|
-
emails.push(this.mapQueueItemToEmail(item));
|
|
100
|
-
}
|
|
88
|
+
const emails = emailServer.getQueueItems().map((item) => this.mapQueueItemToEmail(item));
|
|
101
89
|
|
|
102
90
|
// Sort by createdAt descending (newest first)
|
|
103
91
|
emails.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
@@ -110,12 +98,10 @@ export class EmailOpsHandler {
|
|
|
110
98
|
*/
|
|
111
99
|
private getEmailDetail(emailId: string): interfaces.requests.IEmailDetail | null {
|
|
112
100
|
const emailServer = this.opsServerRef.dcRouterRef.emailServer;
|
|
113
|
-
if (!emailServer
|
|
101
|
+
if (!emailServer) {
|
|
114
102
|
return null;
|
|
115
103
|
}
|
|
116
|
-
|
|
117
|
-
const queue = emailServer.deliveryQueue;
|
|
118
|
-
const item = queue.getItem(emailId);
|
|
104
|
+
const item = emailServer.getQueueItem(emailId);
|
|
119
105
|
|
|
120
106
|
if (!item) {
|
|
121
107
|
return null;
|
|
@@ -530,13 +530,49 @@ export class StatsHandler {
|
|
|
530
530
|
nextRetry?: number;
|
|
531
531
|
}>;
|
|
532
532
|
}> {
|
|
533
|
-
|
|
533
|
+
const emailServer = this.opsServerRef.dcRouterRef.emailServer;
|
|
534
|
+
if (!emailServer) {
|
|
535
|
+
return {
|
|
536
|
+
pending: 0,
|
|
537
|
+
active: 0,
|
|
538
|
+
failed: 0,
|
|
539
|
+
retrying: 0,
|
|
540
|
+
items: [],
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const queueStats = emailServer.getQueueStats();
|
|
545
|
+
const items = emailServer.getQueueItems()
|
|
546
|
+
.sort((a, b) => {
|
|
547
|
+
const left = a.createdAt instanceof Date ? a.createdAt.getTime() : new Date(a.createdAt).getTime();
|
|
548
|
+
const right = b.createdAt instanceof Date ? b.createdAt.getTime() : new Date(b.createdAt).getTime();
|
|
549
|
+
return right - left;
|
|
550
|
+
})
|
|
551
|
+
.slice(0, 50)
|
|
552
|
+
.map((item) => {
|
|
553
|
+
const emailLike = item.processingResult;
|
|
554
|
+
const recipients = Array.isArray(emailLike?.to)
|
|
555
|
+
? emailLike.to
|
|
556
|
+
: Array.isArray(emailLike?.email?.to)
|
|
557
|
+
? emailLike.email.to
|
|
558
|
+
: [];
|
|
559
|
+
const subject = emailLike?.subject || emailLike?.email?.subject || '';
|
|
560
|
+
return {
|
|
561
|
+
id: item.id,
|
|
562
|
+
recipient: recipients[0] || '',
|
|
563
|
+
subject,
|
|
564
|
+
status: item.status,
|
|
565
|
+
attempts: item.attempts,
|
|
566
|
+
nextRetry: item.nextAttempt instanceof Date ? item.nextAttempt.getTime() : undefined,
|
|
567
|
+
};
|
|
568
|
+
});
|
|
569
|
+
|
|
534
570
|
return {
|
|
535
|
-
pending:
|
|
536
|
-
active:
|
|
537
|
-
failed:
|
|
538
|
-
retrying:
|
|
539
|
-
items
|
|
571
|
+
pending: queueStats.status.pending,
|
|
572
|
+
active: queueStats.status.processing,
|
|
573
|
+
failed: queueStats.status.failed,
|
|
574
|
+
retrying: queueStats.status.deferred,
|
|
575
|
+
items,
|
|
540
576
|
};
|
|
541
577
|
}
|
|
542
578
|
|
|
@@ -600,4 +636,4 @@ export class StatsHandler {
|
|
|
600
636
|
],
|
|
601
637
|
};
|
|
602
638
|
}
|
|
603
|
-
}
|
|
639
|
+
}
|