@serve.zone/dcrouter 7.4.2 → 8.0.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 +10755 -3472
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +3 -0
- package/dist_ts/classes.dcrouter.js +43 -3
- package/dist_ts/opsserver/handlers/email-ops.handler.d.ts +22 -2
- package/dist_ts/opsserver/handlers/email-ops.handler.js +166 -159
- package/dist_ts_interfaces/requests/email-ops.d.ts +51 -108
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate.d.ts +2 -16
- package/dist_ts_web/appstate.js +7 -176
- package/dist_ts_web/elements/ops-view-emails.d.ts +8 -31
- package/dist_ts_web/elements/ops-view-emails.js +54 -769
- package/dist_ts_web/elements/ops-view-logs.d.ts +2 -8
- package/dist_ts_web/elements/ops-view-logs.js +4 -101
- package/dist_ts_web/plugins.d.ts +2 -1
- package/dist_ts_web/plugins.js +4 -2
- package/dist_ts_web/router.d.ts +0 -6
- package/dist_ts_web/router.js +7 -81
- package/package.json +3 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +46 -2
- package/ts/opsserver/handlers/email-ops.handler.ts +177 -225
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate.ts +8 -221
- package/ts_web/elements/ops-view-emails.ts +40 -749
- package/ts_web/elements/ops-view-logs.ts +2 -87
- package/ts_web/plugins.ts +4 -0
- package/ts_web/router.ts +6 -81
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as plugins from '../../plugins.js';
|
|
2
2
|
import type { OpsServer } from '../classes.opsserver.js';
|
|
3
3
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
|
4
|
-
import { SecurityLogger } from '../../security/index.js';
|
|
5
4
|
|
|
6
5
|
export class EmailOpsHandler {
|
|
7
6
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
@@ -13,68 +12,24 @@ export class EmailOpsHandler {
|
|
|
13
12
|
}
|
|
14
13
|
|
|
15
14
|
private registerHandlers(): void {
|
|
16
|
-
// Get
|
|
15
|
+
// Get All Emails Handler
|
|
17
16
|
this.typedrouter.addTypedHandler(
|
|
18
|
-
new plugins.typedrequest.TypedHandler<interfaces.requests.
|
|
19
|
-
'
|
|
17
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetAllEmails>(
|
|
18
|
+
'getAllEmails',
|
|
20
19
|
async (dataArg) => {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
return { items: [], total: 0 };
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const queue = emailServer.deliveryQueue;
|
|
27
|
-
const stats = queue.getStats();
|
|
28
|
-
|
|
29
|
-
// Get all queue items and filter by status if provided
|
|
30
|
-
const items = this.getQueueItems(
|
|
31
|
-
dataArg.status,
|
|
32
|
-
dataArg.limit || 50,
|
|
33
|
-
dataArg.offset || 0
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
return {
|
|
37
|
-
items,
|
|
38
|
-
total: stats.queueSize,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
)
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
// Get Sent Emails Handler
|
|
45
|
-
this.typedrouter.addTypedHandler(
|
|
46
|
-
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetSentEmails>(
|
|
47
|
-
'getSentEmails',
|
|
48
|
-
async (dataArg) => {
|
|
49
|
-
const items = this.getQueueItems(
|
|
50
|
-
'delivered',
|
|
51
|
-
dataArg.limit || 50,
|
|
52
|
-
dataArg.offset || 0
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
return {
|
|
56
|
-
items,
|
|
57
|
-
total: items.length, // Note: total would ideally come from a counter
|
|
58
|
-
};
|
|
20
|
+
const emails = this.getAllQueueEmails();
|
|
21
|
+
return { emails };
|
|
59
22
|
}
|
|
60
23
|
)
|
|
61
24
|
);
|
|
62
25
|
|
|
63
|
-
// Get
|
|
26
|
+
// Get Email Detail Handler
|
|
64
27
|
this.typedrouter.addTypedHandler(
|
|
65
|
-
new plugins.typedrequest.TypedHandler<interfaces.requests.
|
|
66
|
-
'
|
|
28
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetEmailDetail>(
|
|
29
|
+
'getEmailDetail',
|
|
67
30
|
async (dataArg) => {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
dataArg.limit || 50,
|
|
71
|
-
dataArg.offset || 0
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
items,
|
|
76
|
-
total: items.length,
|
|
77
|
-
};
|
|
31
|
+
const email = this.getEmailDetail(dataArg.emailId);
|
|
32
|
+
return { email };
|
|
78
33
|
}
|
|
79
34
|
)
|
|
80
35
|
);
|
|
@@ -101,17 +56,12 @@ export class EmailOpsHandler {
|
|
|
101
56
|
}
|
|
102
57
|
|
|
103
58
|
try {
|
|
104
|
-
// Re-enqueue the failed email by creating a new queue entry
|
|
105
|
-
// with the same data but reset attempt count
|
|
106
59
|
const newQueueId = await queue.enqueue(
|
|
107
60
|
item.processingResult,
|
|
108
61
|
item.processingMode,
|
|
109
62
|
item.route
|
|
110
63
|
);
|
|
111
|
-
|
|
112
|
-
// Optionally remove the old failed entry
|
|
113
64
|
await queue.removeItem(dataArg.emailId);
|
|
114
|
-
|
|
115
65
|
return { success: true, newQueueId };
|
|
116
66
|
} catch (error) {
|
|
117
67
|
return {
|
|
@@ -122,197 +72,199 @@ export class EmailOpsHandler {
|
|
|
122
72
|
}
|
|
123
73
|
)
|
|
124
74
|
);
|
|
75
|
+
}
|
|
125
76
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
level?: any;
|
|
135
|
-
type?: any;
|
|
136
|
-
} = {};
|
|
137
|
-
|
|
138
|
-
if (dataArg.level) {
|
|
139
|
-
filter.level = dataArg.level;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (dataArg.type) {
|
|
143
|
-
filter.type = dataArg.type;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const incidents = securityLogger.getRecentEvents(
|
|
147
|
-
dataArg.limit || 100,
|
|
148
|
-
Object.keys(filter).length > 0 ? filter : undefined
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
return {
|
|
152
|
-
incidents: incidents.map(event => ({
|
|
153
|
-
timestamp: event.timestamp,
|
|
154
|
-
level: event.level as interfaces.requests.TSecurityLogLevel,
|
|
155
|
-
type: event.type as interfaces.requests.TSecurityEventType,
|
|
156
|
-
message: event.message,
|
|
157
|
-
details: event.details,
|
|
158
|
-
ipAddress: event.ipAddress,
|
|
159
|
-
userId: event.userId,
|
|
160
|
-
sessionId: event.sessionId,
|
|
161
|
-
emailId: event.emailId,
|
|
162
|
-
domain: event.domain,
|
|
163
|
-
action: event.action,
|
|
164
|
-
result: event.result,
|
|
165
|
-
success: event.success,
|
|
166
|
-
})),
|
|
167
|
-
total: incidents.length,
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
)
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
// Get Bounce Records Handler
|
|
174
|
-
this.typedrouter.addTypedHandler(
|
|
175
|
-
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetBounceRecords>(
|
|
176
|
-
'getBounceRecords',
|
|
177
|
-
async (dataArg) => {
|
|
178
|
-
const emailServer = this.opsServerRef.dcRouterRef.emailServer;
|
|
179
|
-
|
|
180
|
-
if (!emailServer) {
|
|
181
|
-
return { records: [], suppressionList: [], total: 0 };
|
|
182
|
-
}
|
|
77
|
+
/**
|
|
78
|
+
* Get all queue items mapped to catalog IEmail format
|
|
79
|
+
*/
|
|
80
|
+
private getAllQueueEmails(): interfaces.requests.IEmail[] {
|
|
81
|
+
const emailServer = this.opsServerRef.dcRouterRef.emailServer;
|
|
82
|
+
if (!emailServer?.deliveryQueue) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
183
85
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const hardBouncedAddresses = emailServer.getHardBouncedAddresses();
|
|
187
|
-
|
|
188
|
-
// Create bounce records from the available data
|
|
189
|
-
const records: interfaces.requests.IBounceRecord[] = [];
|
|
190
|
-
|
|
191
|
-
for (const email of hardBouncedAddresses) {
|
|
192
|
-
const bounceInfo = emailServer.getBounceHistory(email);
|
|
193
|
-
if (bounceInfo) {
|
|
194
|
-
records.push({
|
|
195
|
-
id: `bounce-${email}`,
|
|
196
|
-
recipient: email,
|
|
197
|
-
sender: '',
|
|
198
|
-
domain: email.split('@')[1] || '',
|
|
199
|
-
bounceType: (bounceInfo as any).type as interfaces.requests.TBounceType,
|
|
200
|
-
bounceCategory: (bounceInfo as any).category as interfaces.requests.TBounceCategory,
|
|
201
|
-
timestamp: (bounceInfo as any).lastBounce,
|
|
202
|
-
processed: true,
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
}
|
|
86
|
+
const queue = emailServer.deliveryQueue;
|
|
87
|
+
const queueMap = (queue as any).queue as Map<string, any>;
|
|
206
88
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const paginatedRecords = records.slice(offset, offset + limit);
|
|
89
|
+
if (!queueMap) {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
211
92
|
|
|
212
|
-
|
|
213
|
-
records: paginatedRecords,
|
|
214
|
-
suppressionList,
|
|
215
|
-
total: records.length,
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
)
|
|
219
|
-
);
|
|
93
|
+
const emails: interfaces.requests.IEmail[] = [];
|
|
220
94
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
'removeFromSuppressionList',
|
|
225
|
-
async (dataArg) => {
|
|
226
|
-
const emailServer = this.opsServerRef.dcRouterRef.emailServer;
|
|
95
|
+
for (const [id, item] of queueMap.entries()) {
|
|
96
|
+
emails.push(this.mapQueueItemToEmail(item));
|
|
97
|
+
}
|
|
227
98
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
99
|
+
// Sort by createdAt descending (newest first)
|
|
100
|
+
emails.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
231
101
|
|
|
232
|
-
|
|
233
|
-
emailServer.removeFromSuppressionList(dataArg.email);
|
|
234
|
-
return { success: true };
|
|
235
|
-
} catch (error) {
|
|
236
|
-
return {
|
|
237
|
-
success: false,
|
|
238
|
-
error: error instanceof Error ? error.message : 'Failed to remove from suppression list'
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
)
|
|
243
|
-
);
|
|
102
|
+
return emails;
|
|
244
103
|
}
|
|
245
104
|
|
|
246
105
|
/**
|
|
247
|
-
*
|
|
106
|
+
* Get a single email detail by ID
|
|
248
107
|
*/
|
|
249
|
-
private
|
|
250
|
-
status?: interfaces.requests.TEmailQueueStatus,
|
|
251
|
-
limit: number = 50,
|
|
252
|
-
offset: number = 0
|
|
253
|
-
): interfaces.requests.IEmailQueueItem[] {
|
|
108
|
+
private getEmailDetail(emailId: string): interfaces.requests.IEmailDetail | null {
|
|
254
109
|
const emailServer = this.opsServerRef.dcRouterRef.emailServer;
|
|
255
110
|
if (!emailServer?.deliveryQueue) {
|
|
256
|
-
return
|
|
111
|
+
return null;
|
|
257
112
|
}
|
|
258
113
|
|
|
259
114
|
const queue = emailServer.deliveryQueue;
|
|
260
|
-
const
|
|
115
|
+
const item = queue.getItem(emailId);
|
|
261
116
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const queueMap = (queue as any).queue as Map<string, any>;
|
|
265
|
-
|
|
266
|
-
if (!queueMap) {
|
|
267
|
-
return [];
|
|
117
|
+
if (!item) {
|
|
118
|
+
return null;
|
|
268
119
|
}
|
|
269
120
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
121
|
+
return this.mapQueueItemToEmailDetail(item);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Map a queue item to catalog IEmail format
|
|
126
|
+
*/
|
|
127
|
+
private mapQueueItemToEmail(item: any): interfaces.requests.IEmail {
|
|
128
|
+
const processingResult = item.processingResult;
|
|
129
|
+
let from = '';
|
|
130
|
+
let to = '';
|
|
131
|
+
let subject = '';
|
|
132
|
+
let messageId = '';
|
|
133
|
+
let size = '0 B';
|
|
134
|
+
|
|
135
|
+
if (processingResult) {
|
|
136
|
+
if (processingResult.email) {
|
|
137
|
+
from = processingResult.email.from || '';
|
|
138
|
+
to = (processingResult.email.to || [])[0] || '';
|
|
139
|
+
subject = processingResult.email.subject || '';
|
|
140
|
+
} else if (processingResult.from) {
|
|
141
|
+
from = processingResult.from;
|
|
142
|
+
to = (processingResult.to || [])[0] || '';
|
|
143
|
+
subject = processingResult.subject || '';
|
|
275
144
|
}
|
|
276
145
|
|
|
277
|
-
//
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
if (processingResult) {
|
|
284
|
-
// Check if it's an Email object or raw email data
|
|
285
|
-
if (processingResult.email) {
|
|
286
|
-
from = processingResult.email.from || '';
|
|
287
|
-
to = processingResult.email.to || [];
|
|
288
|
-
subject = processingResult.email.subject || '';
|
|
289
|
-
} else if (processingResult.from) {
|
|
290
|
-
from = processingResult.from;
|
|
291
|
-
to = processingResult.to || [];
|
|
292
|
-
subject = processingResult.subject || '';
|
|
146
|
+
// Try to get messageId
|
|
147
|
+
if (typeof processingResult.getMessageId === 'function') {
|
|
148
|
+
try {
|
|
149
|
+
messageId = processingResult.getMessageId() || '';
|
|
150
|
+
} catch {
|
|
151
|
+
messageId = '';
|
|
293
152
|
}
|
|
294
153
|
}
|
|
295
154
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
subject,
|
|
309
|
-
});
|
|
155
|
+
// Compute approximate size
|
|
156
|
+
const textLen = processingResult.text?.length || 0;
|
|
157
|
+
const htmlLen = processingResult.html?.length || 0;
|
|
158
|
+
let attachSize = 0;
|
|
159
|
+
if (typeof processingResult.getAttachmentsSize === 'function') {
|
|
160
|
+
try {
|
|
161
|
+
attachSize = processingResult.getAttachmentsSize() || 0;
|
|
162
|
+
} catch {
|
|
163
|
+
attachSize = 0;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
size = this.formatSize(textLen + htmlLen + attachSize);
|
|
310
167
|
}
|
|
311
168
|
|
|
312
|
-
//
|
|
313
|
-
|
|
169
|
+
// Map queue status to catalog TEmailStatus
|
|
170
|
+
const status = this.mapStatus(item.status);
|
|
171
|
+
|
|
172
|
+
const createdAt = item.createdAt instanceof Date ? item.createdAt.getTime() : item.createdAt;
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
id: item.id,
|
|
176
|
+
direction: 'outbound' as interfaces.requests.TEmailDirection,
|
|
177
|
+
status,
|
|
178
|
+
from,
|
|
179
|
+
to,
|
|
180
|
+
subject,
|
|
181
|
+
timestamp: new Date(createdAt).toISOString(),
|
|
182
|
+
messageId,
|
|
183
|
+
size,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Map a queue item to catalog IEmailDetail format
|
|
189
|
+
*/
|
|
190
|
+
private mapQueueItemToEmailDetail(item: any): interfaces.requests.IEmailDetail {
|
|
191
|
+
const base = this.mapQueueItemToEmail(item);
|
|
192
|
+
const processingResult = item.processingResult;
|
|
193
|
+
|
|
194
|
+
let toList: string[] = [];
|
|
195
|
+
let cc: string[] = [];
|
|
196
|
+
let headers: Record<string, string> = {};
|
|
197
|
+
let body = '';
|
|
198
|
+
|
|
199
|
+
if (processingResult) {
|
|
200
|
+
if (processingResult.email) {
|
|
201
|
+
toList = processingResult.email.to || [];
|
|
202
|
+
cc = processingResult.email.cc || [];
|
|
203
|
+
} else {
|
|
204
|
+
toList = processingResult.to || [];
|
|
205
|
+
cc = processingResult.cc || [];
|
|
206
|
+
}
|
|
314
207
|
|
|
315
|
-
|
|
316
|
-
|
|
208
|
+
headers = processingResult.headers || {};
|
|
209
|
+
body = processingResult.html || processingResult.text || '';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
...base,
|
|
214
|
+
toList,
|
|
215
|
+
cc,
|
|
216
|
+
smtpLog: [],
|
|
217
|
+
connectionInfo: {
|
|
218
|
+
sourceIp: '',
|
|
219
|
+
sourceHostname: '',
|
|
220
|
+
destinationIp: '',
|
|
221
|
+
destinationPort: 0,
|
|
222
|
+
tlsVersion: '',
|
|
223
|
+
tlsCipher: '',
|
|
224
|
+
authenticated: false,
|
|
225
|
+
authMethod: '',
|
|
226
|
+
authUser: '',
|
|
227
|
+
},
|
|
228
|
+
authenticationResults: {
|
|
229
|
+
spf: 'none',
|
|
230
|
+
spfDomain: '',
|
|
231
|
+
dkim: 'none',
|
|
232
|
+
dkimDomain: '',
|
|
233
|
+
dmarc: 'none',
|
|
234
|
+
dmarcPolicy: '',
|
|
235
|
+
},
|
|
236
|
+
rejectionReason: item.status === 'failed' ? item.lastError : undefined,
|
|
237
|
+
bounceMessage: item.status === 'failed' ? item.lastError : undefined,
|
|
238
|
+
headers,
|
|
239
|
+
body,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Map queue status to catalog TEmailStatus
|
|
245
|
+
*/
|
|
246
|
+
private mapStatus(queueStatus: string): interfaces.requests.TEmailStatus {
|
|
247
|
+
switch (queueStatus) {
|
|
248
|
+
case 'pending':
|
|
249
|
+
case 'processing':
|
|
250
|
+
return 'pending';
|
|
251
|
+
case 'delivered':
|
|
252
|
+
return 'delivered';
|
|
253
|
+
case 'failed':
|
|
254
|
+
return 'bounced';
|
|
255
|
+
case 'deferred':
|
|
256
|
+
return 'deferred';
|
|
257
|
+
default:
|
|
258
|
+
return 'pending';
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Format byte size to human-readable string
|
|
264
|
+
*/
|
|
265
|
+
private formatSize(bytes: number): string {
|
|
266
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
267
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
268
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
317
269
|
}
|
|
318
270
|
}
|