@serve.zone/dcrouter 7.4.3 → 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.
@@ -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 Queued Emails Handler
15
+ // Get All Emails Handler
17
16
  this.typedrouter.addTypedHandler(
18
- new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetQueuedEmails>(
19
- 'getQueuedEmails',
17
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetAllEmails>(
18
+ 'getAllEmails',
20
19
  async (dataArg) => {
21
- const emailServer = this.opsServerRef.dcRouterRef.emailServer;
22
- if (!emailServer?.deliveryQueue) {
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 Failed Emails Handler
26
+ // Get Email Detail Handler
64
27
  this.typedrouter.addTypedHandler(
65
- new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetFailedEmails>(
66
- 'getFailedEmails',
28
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetEmailDetail>(
29
+ 'getEmailDetail',
67
30
  async (dataArg) => {
68
- const items = this.getQueueItems(
69
- 'failed',
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
- // Get Security Incidents Handler
127
- this.typedrouter.addTypedHandler(
128
- new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetSecurityIncidents>(
129
- 'getSecurityIncidents',
130
- async (dataArg) => {
131
- const securityLogger = SecurityLogger.getInstance();
132
-
133
- const filter: {
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
- // Use smartmta's public API for bounce/suppression data
185
- const suppressionList = emailServer.getSuppressionList();
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
- // Apply limit and offset
208
- const limit = dataArg.limit || 50;
209
- const offset = dataArg.offset || 0;
210
- const paginatedRecords = records.slice(offset, offset + limit);
89
+ if (!queueMap) {
90
+ return [];
91
+ }
211
92
 
212
- return {
213
- records: paginatedRecords,
214
- suppressionList,
215
- total: records.length,
216
- };
217
- }
218
- )
219
- );
93
+ const emails: interfaces.requests.IEmail[] = [];
220
94
 
221
- // Remove from Suppression List Handler
222
- this.typedrouter.addTypedHandler(
223
- new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RemoveFromSuppressionList>(
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
- if (!emailServer) {
229
- return { success: false, error: 'Email server not available' };
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
- try {
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
- * Helper method to get queue items with filtering and pagination
106
+ * Get a single email detail by ID
248
107
  */
249
- private getQueueItems(
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 items: interfaces.requests.IEmailQueueItem[] = [];
115
+ const item = queue.getItem(emailId);
261
116
 
262
- // Access the internal queue map via reflection
263
- // This is necessary because the queue doesn't expose iteration methods
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
- // Filter and convert items
271
- for (const [id, item] of queueMap.entries()) {
272
- // Apply status filter if provided
273
- if (status && item.status !== status) {
274
- continue;
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
- // Extract email details from processingResult if available
278
- const processingResult = item.processingResult;
279
- let from = '';
280
- let to: string[] = [];
281
- let subject = '';
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
- items.push({
297
- id: item.id,
298
- processingMode: item.processingMode,
299
- status: item.status,
300
- attempts: item.attempts,
301
- nextAttempt: item.nextAttempt instanceof Date ? item.nextAttempt.getTime() : item.nextAttempt,
302
- lastError: item.lastError,
303
- createdAt: item.createdAt instanceof Date ? item.createdAt.getTime() : item.createdAt,
304
- updatedAt: item.updatedAt instanceof Date ? item.updatedAt.getTime() : item.updatedAt,
305
- deliveredAt: item.deliveredAt instanceof Date ? item.deliveredAt.getTime() : item.deliveredAt,
306
- from,
307
- to,
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
- // Sort by createdAt descending (newest first)
313
- items.sort((a, b) => b.createdAt - a.createdAt);
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
- // Apply pagination
316
- return items.slice(offset, offset + limit);
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
  }
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '7.4.3',
6
+ version: '8.0.0',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }