@serve.zone/dcrouter 18.0.2 → 18.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.
Files changed (103) hide show
  1. package/deno.json +1 -1
  2. package/dist_serve/bundle.js +1529 -1382
  3. package/dist_ts/00_commitinfo_data.js +1 -1
  4. package/dist_ts/classes.dcrouter.d.ts +7 -2
  5. package/dist_ts/classes.dcrouter.js +61 -9
  6. package/dist_ts/db/documents/classes.authentication-event.doc.d.ts +25 -0
  7. package/dist_ts/db/documents/classes.authentication-event.doc.js +273 -0
  8. package/dist_ts/db/documents/classes.email-traffic-bucket.doc.d.ts +25 -0
  9. package/dist_ts/db/documents/classes.email-traffic-bucket.doc.js +200 -0
  10. package/dist_ts/db/documents/classes.smtp-account.doc.d.ts +31 -0
  11. package/dist_ts/db/documents/classes.smtp-account.doc.js +176 -0
  12. package/dist_ts/db/documents/index.d.ts +3 -0
  13. package/dist_ts/db/documents/index.js +4 -1
  14. package/dist_ts/email/classes.accepted-email-spool.d.ts +1 -0
  15. package/dist_ts/email/classes.accepted-email-spool.js +9 -1
  16. package/dist_ts/email/classes.email-settings.manager.d.ts +7 -0
  17. package/dist_ts/email/classes.email-settings.manager.js +30 -1
  18. package/dist_ts/email/classes.smtp-account.manager.d.ts +110 -0
  19. package/dist_ts/email/classes.smtp-account.manager.js +507 -0
  20. package/dist_ts/email/classes.workapp-mail-manager.d.ts +23 -4
  21. package/dist_ts/email/classes.workapp-mail-manager.js +39 -46
  22. package/dist_ts/email/index.d.ts +2 -0
  23. package/dist_ts/email/index.js +3 -1
  24. package/dist_ts/email/smtp-scram.d.ts +13 -0
  25. package/dist_ts/email/smtp-scram.js +19 -0
  26. package/dist_ts/monitoring/classes.metricsmanager.d.ts +19 -0
  27. package/dist_ts/monitoring/classes.metricsmanager.js +240 -44
  28. package/dist_ts/opsserver/classes.opsserver.d.ts +1 -0
  29. package/dist_ts/opsserver/classes.opsserver.js +3 -1
  30. package/dist_ts/opsserver/handlers/admin.handler.d.ts +1 -0
  31. package/dist_ts/opsserver/handlers/admin.handler.js +61 -14
  32. package/dist_ts/opsserver/handlers/email-settings.handler.js +14 -2
  33. package/dist_ts/opsserver/handlers/index.d.ts +1 -0
  34. package/dist_ts/opsserver/handlers/index.js +2 -1
  35. package/dist_ts/opsserver/handlers/security.handler.js +21 -3
  36. package/dist_ts/opsserver/handlers/smtp-account.handler.d.ts +20 -0
  37. package/dist_ts/opsserver/handlers/smtp-account.handler.js +155 -0
  38. package/dist_ts/opsserver/handlers/stats.handler.js +27 -10
  39. package/dist_ts/security/classes.authentication-event-manager.d.ts +35 -0
  40. package/dist_ts/security/classes.authentication-event-manager.js +185 -0
  41. package/dist_ts/security/index.d.ts +1 -0
  42. package/dist_ts/security/index.js +2 -1
  43. package/dist_ts_interfaces/data/email-settings.d.ts +10 -0
  44. package/dist_ts_interfaces/data/index.d.ts +1 -0
  45. package/dist_ts_interfaces/data/index.js +2 -1
  46. package/dist_ts_interfaces/data/route-management.d.ts +1 -1
  47. package/dist_ts_interfaces/data/route-management.js +3 -1
  48. package/dist_ts_interfaces/data/smtp-account.d.ts +50 -0
  49. package/dist_ts_interfaces/data/smtp-account.js +9 -0
  50. package/dist_ts_interfaces/data/stats.d.ts +16 -0
  51. package/dist_ts_interfaces/requests/index.d.ts +1 -0
  52. package/dist_ts_interfaces/requests/index.js +2 -1
  53. package/dist_ts_interfaces/requests/smtp-accounts.d.ts +125 -0
  54. package/dist_ts_interfaces/requests/smtp-accounts.js +2 -0
  55. package/dist_ts_migrations/index.js +186 -13
  56. package/dist_ts_web/00_commitinfo_data.js +1 -1
  57. package/dist_ts_web/appstate/smtp-accounts.d.ts +38 -0
  58. package/dist_ts_web/appstate/smtp-accounts.js +94 -0
  59. package/dist_ts_web/appstate.d.ts +1 -0
  60. package/dist_ts_web/appstate.js +2 -1
  61. package/dist_ts_web/elements/email/index.d.ts +1 -0
  62. package/dist_ts_web/elements/email/index.js +2 -1
  63. package/dist_ts_web/elements/email/ops-view-smtp-accounts.d.ts +25 -0
  64. package/dist_ts_web/elements/email/ops-view-smtp-accounts.js +598 -0
  65. package/dist_ts_web/elements/ops-dashboard.js +3 -1
  66. package/dist_ts_web/elements/overview/ops-view-overview.js +3 -2
  67. package/dist_ts_web/elements/security/ops-view-security-authentication.js +20 -13
  68. package/dist_ts_web/elements/security/ops-view-security-overview.js +8 -11
  69. package/dist_ts_web/router.js +2 -2
  70. package/package.json +2 -2
  71. package/readme.md +20 -0
  72. package/ts/00_commitinfo_data.ts +1 -1
  73. package/ts/classes.dcrouter.ts +66 -8
  74. package/ts/db/documents/classes.authentication-event.doc.ts +248 -0
  75. package/ts/db/documents/classes.email-traffic-bucket.doc.ts +161 -0
  76. package/ts/db/documents/classes.smtp-account.doc.ts +103 -0
  77. package/ts/db/documents/index.ts +3 -0
  78. package/ts/email/classes.accepted-email-spool.ts +8 -0
  79. package/ts/email/classes.email-settings.manager.ts +34 -0
  80. package/ts/email/classes.smtp-account.manager.ts +601 -0
  81. package/ts/email/classes.workapp-mail-manager.ts +53 -55
  82. package/ts/email/index.ts +2 -0
  83. package/ts/email/smtp-scram.ts +24 -0
  84. package/ts/monitoring/classes.metricsmanager.ts +287 -50
  85. package/ts/opsserver/classes.opsserver.ts +2 -0
  86. package/ts/opsserver/handlers/admin.handler.ts +74 -15
  87. package/ts/opsserver/handlers/email-settings.handler.ts +13 -1
  88. package/ts/opsserver/handlers/index.ts +1 -0
  89. package/ts/opsserver/handlers/security.handler.ts +23 -2
  90. package/ts/opsserver/handlers/smtp-account.handler.ts +199 -0
  91. package/ts/opsserver/handlers/stats.handler.ts +29 -9
  92. package/ts/security/classes.authentication-event-manager.ts +221 -0
  93. package/ts/security/index.ts +1 -0
  94. package/ts_web/00_commitinfo_data.ts +1 -1
  95. package/ts_web/appstate/smtp-accounts.ts +143 -0
  96. package/ts_web/appstate.ts +1 -0
  97. package/ts_web/elements/email/index.ts +1 -0
  98. package/ts_web/elements/email/ops-view-smtp-accounts.ts +586 -0
  99. package/ts_web/elements/ops-dashboard.ts +2 -0
  100. package/ts_web/elements/overview/ops-view-overview.ts +2 -1
  101. package/ts_web/elements/security/ops-view-security-authentication.ts +20 -13
  102. package/ts_web/elements/security/ops-view-security-overview.ts +7 -12
  103. package/ts_web/router.ts +1 -1
@@ -18,6 +18,11 @@ type TAdminUser = {
18
18
  authSources?: Array<'local' | 'idp.global'>;
19
19
  };
20
20
 
21
+ type TAuthenticatedAdmin = {
22
+ user: TAdminUser;
23
+ authSource: 'local' | 'idp.global';
24
+ };
25
+
21
26
  export class AdminHandler {
22
27
  public typedrouter = new plugins.typedrequest.TypedRouter();
23
28
 
@@ -330,25 +335,59 @@ export class AdminHandler {
330
335
  new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_AdminLoginWithUsernameAndPassword>(
331
336
  'adminLoginWithUsernameAndPassword',
332
337
  async (dataArg) => {
338
+ const requestedAuthSource = dataArg.authSource || 'auto';
339
+ let authenticatedAdmin: TAuthenticatedAdmin | null;
333
340
  try {
334
- const user = await this.authenticateUser({
341
+ authenticatedAdmin = await this.authenticateUser({
335
342
  username: dataArg.username,
336
343
  password: dataArg.password,
337
- authSource: dataArg.authSource,
344
+ authSource: requestedAuthSource,
338
345
  });
339
- if (!user) {
340
- throw new plugins.typedrequest.TypedResponseError('login failed');
341
- }
342
-
343
- return {
344
- identity: await this.createIdentityForUser(user),
345
- };
346
346
  } catch (error) {
347
- if (error instanceof plugins.typedrequest.TypedResponseError) {
348
- throw error;
349
- }
347
+ await this.recordAuthenticationAttempt({
348
+ username: dataArg.username,
349
+ success: false,
350
+ requestedAuthSource,
351
+ failureReason: String((error as Error).message || '').includes('database is not ready')
352
+ ? 'serviceUnavailable'
353
+ : 'internalError',
354
+ });
350
355
  throw new plugins.typedrequest.TypedResponseError('login failed');
351
356
  }
357
+
358
+ if (!authenticatedAdmin) {
359
+ await this.recordAuthenticationAttempt({
360
+ username: dataArg.username,
361
+ success: false,
362
+ requestedAuthSource,
363
+ failureReason: 'invalidCredentials',
364
+ });
365
+ throw new plugins.typedrequest.TypedResponseError('login failed');
366
+ }
367
+
368
+ let identity: interfaces.data.IIdentity;
369
+ try {
370
+ identity = await this.createIdentityForUser(authenticatedAdmin.user);
371
+ } catch {
372
+ await this.recordAuthenticationAttempt({
373
+ username: dataArg.username,
374
+ userId: authenticatedAdmin.user.id,
375
+ success: false,
376
+ requestedAuthSource,
377
+ resolvedAuthSource: authenticatedAdmin.authSource,
378
+ failureReason: 'identityIssuanceFailed',
379
+ });
380
+ throw new plugins.typedrequest.TypedResponseError('login failed');
381
+ }
382
+
383
+ await this.recordAuthenticationAttempt({
384
+ username: dataArg.username,
385
+ userId: authenticatedAdmin.user.id,
386
+ success: true,
387
+ requestedAuthSource,
388
+ resolvedAuthSource: authenticatedAdmin.authSource,
389
+ });
390
+ return { identity };
352
391
  }
353
392
  )
354
393
  );
@@ -492,7 +531,7 @@ export class AdminHandler {
492
531
  username: string;
493
532
  password: string;
494
533
  authSource?: interfaces.requests.TAdminLoginAuthSource;
495
- }): Promise<TAdminUser | null> {
534
+ }): Promise<TAuthenticatedAdmin | null> {
496
535
  const accountState = await this.getPersistentAccountState();
497
536
  if (accountState.dbEnabled && !accountState.dbReady) {
498
537
  throw new plugins.typedrequest.TypedResponseError('database is not ready');
@@ -508,17 +547,37 @@ export class AdminHandler {
508
547
  password: optionsArg.password,
509
548
  authSource: optionsArg.authSource || 'auto',
510
549
  });
511
- return result ? this.accountToUser(result.account) : null;
550
+ return result
551
+ ? { user: this.accountToUser(result.account), authSource: result.authSource }
552
+ : null;
512
553
  }
513
554
 
514
555
  for (const [_, userData] of this.users) {
515
556
  if (userData.username === optionsArg.username && userData.password === optionsArg.password) {
516
- return userData;
557
+ return { user: userData, authSource: 'local' };
517
558
  }
518
559
  }
519
560
  return null;
520
561
  }
521
562
 
563
+ private async recordAuthenticationAttempt(optionsArg: {
564
+ username: string;
565
+ userId?: string;
566
+ success: boolean;
567
+ requestedAuthSource: interfaces.requests.TAdminLoginAuthSource;
568
+ resolvedAuthSource?: 'local' | 'idp.global';
569
+ failureReason?: interfaces.data.TAuthenticationFailureReason;
570
+ }): Promise<void> {
571
+ const eventManager = this.opsServerRef.dcRouterRef.authenticationEventManager;
572
+ if (!eventManager) return;
573
+ try {
574
+ await eventManager.recordAttempt(optionsArg);
575
+ } catch (error) {
576
+ // Authentication availability must not depend on telemetry persistence.
577
+ console.warn(`Authentication event recording unavailable: ${(error as Error).message}`);
578
+ }
579
+ }
580
+
522
581
  private async resolveUser(userIdArg: string): Promise<TAdminUser | null> {
523
582
  const accountState = await this.getPersistentAccountState();
524
583
  if (accountState.dbEnabled && !accountState.dbReady) {
@@ -54,9 +54,19 @@ export class EmailSettingsHandler {
54
54
  const mailerBridgeState = dcRouter.emailServer?.getBridgeState() as
55
55
  | interfaces.data.TEmailBridgeState
56
56
  | undefined;
57
+ const smtpAccountCount = dcRouter.smtpAccountManager?.getAccountCount() ?? 0;
58
+ const smtpListenerActive = dcRouter.emailServer
59
+ ? dcRouter.emailServer.isSmtpListenerActive()
60
+ : undefined;
57
61
  const manager = dcRouter.emailSettingsManager;
58
62
  if (manager) {
59
- return { ...manager.getPublicSettings(), outboundEgress, mailerBridgeState };
63
+ return {
64
+ ...manager.getPublicSettings(),
65
+ outboundEgress,
66
+ mailerBridgeState,
67
+ smtpAccountCount,
68
+ ...(smtpListenerActive !== undefined ? { smtpListenerActive } : {}),
69
+ };
60
70
  }
61
71
  const emailConfig = dcRouter.options.emailConfig;
62
72
  const emailPortConfig = dcRouter.options.emailPortConfig;
@@ -75,6 +85,8 @@ export class EmailSettingsHandler {
75
85
  domainCount: emailConfig?.domains?.length || 0,
76
86
  routeCount: emailConfig?.routes?.length || 0,
77
87
  authUserCount: emailConfig?.auth?.users?.length || 0,
88
+ smtpAccountCount,
89
+ ...(smtpListenerActive !== undefined ? { smtpListenerActive } : {}),
78
90
  updatedAt: 0,
79
91
  updatedBy: 'runtime-options',
80
92
  };
@@ -21,6 +21,7 @@ export * from './dns-authority.handler.js';
21
21
  export * from './dns-record.handler.js';
22
22
  export * from './acme-config.handler.js';
23
23
  export * from './email-domain.handler.js';
24
+ export * from './smtp-account.handler.js';
24
25
  export * from './gatewayclient.handler.js';
25
26
  export * from './realtime.handler.js';
26
27
  export * from './config-event.handler.js';
@@ -27,6 +27,9 @@ export class SecurityHandler {
27
27
  malwareDetected: metrics.malwareDetected,
28
28
  phishingDetected: metrics.phishingDetected,
29
29
  authenticationFailures: metrics.authFailures,
30
+ authenticationSuccesses: metrics.authSuccesses,
31
+ authenticationWindowStart: metrics.authWindowStart,
32
+ authenticationEvents: metrics.authEvents,
30
33
  suspiciousActivities: metrics.suspiciousActivities,
31
34
  },
32
35
  trends: dataArg.includeDetails ? {
@@ -309,6 +312,9 @@ export class SecurityHandler {
309
312
  malwareDetected: number;
310
313
  phishingDetected: number;
311
314
  authFailures: number;
315
+ authSuccesses: number;
316
+ authWindowStart: number;
317
+ authEvents: interfaces.data.IAuthenticationEvent[];
312
318
  suspiciousActivities: number;
313
319
  trends: {
314
320
  spam: Array<{ timestamp: number; value: number }>;
@@ -316,6 +322,15 @@ export class SecurityHandler {
316
322
  phishing: Array<{ timestamp: number; value: number }>;
317
323
  };
318
324
  }> {
325
+ const authenticationEventManager = this.opsServerRef.dcRouterRef.authenticationEventManager;
326
+ const authentication = authenticationEventManager
327
+ ? await authenticationEventManager.getWindowStats()
328
+ : {
329
+ windowStart: Date.now() - 86_400_000,
330
+ successes: 0,
331
+ failures: 0,
332
+ events: [],
333
+ };
319
334
  // Get metrics from MetricsManager if available
320
335
  if (this.opsServerRef.dcRouterRef.metricsManager) {
321
336
  const securityStats = await this.opsServerRef.dcRouterRef.metricsManager.getSecurityStats();
@@ -328,7 +343,10 @@ export class SecurityHandler {
328
343
  },
329
344
  malwareDetected: securityStats.malwareDetected,
330
345
  phishingDetected: securityStats.phishingDetected,
331
- authFailures: securityStats.authFailures,
346
+ authFailures: authentication.failures,
347
+ authSuccesses: authentication.successes,
348
+ authWindowStart: authentication.windowStart,
349
+ authEvents: authentication.events,
332
350
  suspiciousActivities: 0,
333
351
  trends: {
334
352
  spam: [],
@@ -348,7 +366,10 @@ export class SecurityHandler {
348
366
  },
349
367
  malwareDetected: 0,
350
368
  phishingDetected: 0,
351
- authFailures: 0,
369
+ authFailures: authentication.failures,
370
+ authSuccesses: authentication.successes,
371
+ authWindowStart: authentication.windowStart,
372
+ authEvents: authentication.events,
352
373
  suspiciousActivities: 0,
353
374
  trends: {
354
375
  spam: [],
@@ -0,0 +1,199 @@
1
+ import * as plugins from '../../plugins.js';
2
+ import type { OpsServer } from '../classes.opsserver.js';
3
+ import * as interfaces from '../../../ts_interfaces/index.js';
4
+ import { requireOpsAuth } from '../helpers/auth.js';
5
+ import { logger } from '../../logger.js';
6
+
7
+ /**
8
+ * CRUD handler for operator-managed SMTP submission accounts, plus the
9
+ * legacy-route retirement lever.
10
+ *
11
+ * Auth: admin JWT or API token with `smtp-accounts:read` / `smtp-accounts:write`
12
+ * scope; every write additionally requires an admin identity when a JWT is
13
+ * used. Successful admin actions are audit-logged to the ops log.
14
+ */
15
+ export class SmtpAccountHandler {
16
+ public typedrouter = new plugins.typedrequest.TypedRouter();
17
+
18
+ constructor(private opsServerRef: OpsServer) {
19
+ this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
20
+ this.registerHandlers();
21
+ }
22
+
23
+ private async requireAuth(
24
+ request: { identity?: interfaces.data.IIdentity; apiToken?: string },
25
+ requiredScope: interfaces.data.TApiTokenScope,
26
+ ): Promise<string> {
27
+ const auth = await requireOpsAuth(this.opsServerRef, request, {
28
+ scope: requiredScope,
29
+ requireAdminIdentity: requiredScope.endsWith(':write'),
30
+ });
31
+ return auth.userId;
32
+ }
33
+
34
+ private get manager() {
35
+ return this.opsServerRef.dcRouterRef.smtpAccountManager;
36
+ }
37
+
38
+ private failure(errorArg: unknown, fallbackMessageArg: string): interfaces.requests.ISmtpAccountActionResult {
39
+ return {
40
+ success: false,
41
+ message: errorArg instanceof Error ? errorArg.message : fallbackMessageArg,
42
+ code: 'SMTP_ACCOUNT_ACTION_FAILED',
43
+ };
44
+ }
45
+
46
+ private registerHandlers(): void {
47
+ this.typedrouter.addTypedHandler(
48
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetSmtpAccounts>(
49
+ 'getSmtpAccounts',
50
+ async (dataArg) => {
51
+ await this.requireAuth(dataArg, 'smtp-accounts:read');
52
+ if (!this.manager?.isStarted()) return { accounts: [] };
53
+ return { accounts: await this.manager.listAccounts() };
54
+ },
55
+ ),
56
+ );
57
+
58
+ this.typedrouter.addTypedHandler(
59
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateSmtpAccount>(
60
+ 'createSmtpAccount',
61
+ async (dataArg) => {
62
+ const userId = await this.requireAuth(dataArg, 'smtp-accounts:write');
63
+ if (!this.manager?.isStarted()) {
64
+ return { success: false, message: 'SmtpAccountManager not initialized', code: 'SMTP_ACCOUNT_MANAGER_UNAVAILABLE' };
65
+ }
66
+ try {
67
+ const result = await this.manager.createAccount({
68
+ username: dataArg.username,
69
+ description: dataArg.description,
70
+ senderScope: dataArg.senderScope,
71
+ recipientScope: dataArg.recipientScope,
72
+ mailPolicy: dataArg.mailPolicy,
73
+ createdBy: userId,
74
+ });
75
+ return {
76
+ success: true,
77
+ warnings: result.warnings.length > 0 ? result.warnings : undefined,
78
+ account: result.account,
79
+ password: result.password,
80
+ };
81
+ } catch (err: unknown) {
82
+ return this.failure(err, 'Failed to create SMTP account');
83
+ }
84
+ },
85
+ ),
86
+ );
87
+
88
+ this.typedrouter.addTypedHandler(
89
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateSmtpAccount>(
90
+ 'updateSmtpAccount',
91
+ async (dataArg) => {
92
+ const userId = await this.requireAuth(dataArg, 'smtp-accounts:write');
93
+ if (!this.manager?.isStarted()) {
94
+ return { success: false, message: 'SmtpAccountManager not initialized', code: 'SMTP_ACCOUNT_MANAGER_UNAVAILABLE' };
95
+ }
96
+ try {
97
+ const result = await this.manager.updateAccount(dataArg.id, {
98
+ description: dataArg.description,
99
+ senderScope: dataArg.senderScope,
100
+ recipientScope: dataArg.recipientScope,
101
+ mailPolicy: dataArg.mailPolicy,
102
+ }, userId);
103
+ return {
104
+ success: true,
105
+ warnings: result.warnings.length > 0 ? result.warnings : undefined,
106
+ account: result.account,
107
+ };
108
+ } catch (err: unknown) {
109
+ return this.failure(err, 'Failed to update SMTP account');
110
+ }
111
+ },
112
+ ),
113
+ );
114
+
115
+ this.typedrouter.addTypedHandler(
116
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ToggleSmtpAccount>(
117
+ 'toggleSmtpAccount',
118
+ async (dataArg) => {
119
+ const userId = await this.requireAuth(dataArg, 'smtp-accounts:write');
120
+ if (!this.manager?.isStarted()) {
121
+ return { success: false, message: 'SmtpAccountManager not initialized', code: 'SMTP_ACCOUNT_MANAGER_UNAVAILABLE' };
122
+ }
123
+ try {
124
+ const result = await this.manager.toggleAccount(dataArg.id, dataArg.enabled, userId);
125
+ return { success: true, account: result.account };
126
+ } catch (err: unknown) {
127
+ return this.failure(err, 'Failed to toggle SMTP account');
128
+ }
129
+ },
130
+ ),
131
+ );
132
+
133
+ this.typedrouter.addTypedHandler(
134
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RotateSmtpAccountPassword>(
135
+ 'rotateSmtpAccountPassword',
136
+ async (dataArg) => {
137
+ const userId = await this.requireAuth(dataArg, 'smtp-accounts:write');
138
+ if (!this.manager?.isStarted()) {
139
+ return { success: false, message: 'SmtpAccountManager not initialized', code: 'SMTP_ACCOUNT_MANAGER_UNAVAILABLE' };
140
+ }
141
+ try {
142
+ const result = await this.manager.rotatePassword(dataArg.id, userId);
143
+ return { success: true, account: result.account, password: result.password };
144
+ } catch (err: unknown) {
145
+ return this.failure(err, 'Failed to rotate SMTP account password');
146
+ }
147
+ },
148
+ ),
149
+ );
150
+
151
+ this.typedrouter.addTypedHandler(
152
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteSmtpAccount>(
153
+ 'deleteSmtpAccount',
154
+ async (dataArg) => {
155
+ const userId = await this.requireAuth(dataArg, 'smtp-accounts:write');
156
+ if (!this.manager?.isStarted()) {
157
+ return { success: false, message: 'SmtpAccountManager not initialized', code: 'SMTP_ACCOUNT_MANAGER_UNAVAILABLE' };
158
+ }
159
+ try {
160
+ await this.manager.deleteAccount(dataArg.id, userId);
161
+ return { success: true };
162
+ } catch (err: unknown) {
163
+ return this.failure(err, 'Failed to delete SMTP account');
164
+ }
165
+ },
166
+ ),
167
+ );
168
+
169
+ // Internal-relay retirement lever: removes one persisted legacy route by
170
+ // exact name through the standard settings restart path. Guarded in
171
+ // EmailSettingsManager (unknown names and generated managed names fail).
172
+ this.typedrouter.addTypedHandler(
173
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RemoveLegacyEmailRoute>(
174
+ 'removeLegacyEmailRoute',
175
+ async (dataArg) => {
176
+ const userId = await this.requireAuth(dataArg, 'smtp-accounts:write');
177
+ const routeName = (dataArg.routeName || '').trim();
178
+ logger.log('info', `removeLegacyEmailRoute invoked by ${userId} for route '${routeName}'`);
179
+ if (!routeName) {
180
+ return { success: false, message: 'routeName is required', code: 'SMTP_ACCOUNT_ACTION_FAILED' };
181
+ }
182
+ if (!this.opsServerRef.dcRouterRef.emailSettingsManager) {
183
+ return { success: false, message: 'EmailSettingsManager not initialized', code: 'EMAIL_SETTINGS_MANAGER_UNAVAILABLE' };
184
+ }
185
+ try {
186
+ await this.opsServerRef.dcRouterRef.updateEmailServerSettings(
187
+ { removeRouteNames: [routeName] },
188
+ userId,
189
+ );
190
+ logger.log('info', `Legacy email route '${routeName}' removed from persisted settings by ${userId}`);
191
+ return { success: true, message: `Route '${routeName}' removed` };
192
+ } catch (err: unknown) {
193
+ return this.failure(err, 'Failed to remove legacy email route');
194
+ }
195
+ },
196
+ ),
197
+ );
198
+ }
199
+ }
@@ -66,10 +66,10 @@ export class StatsHandler {
66
66
  stats: {
67
67
  sent: stats.sentToday,
68
68
  received: stats.receivedToday,
69
- bounced: Math.floor(stats.sentToday * stats.bounceRate / 100),
69
+ bounced: stats.bouncedToday,
70
70
  queued: stats.queueSize,
71
- failed: 0,
72
- averageDeliveryTime: 0,
71
+ failed: stats.failedToday,
72
+ averageDeliveryTime: stats.averageDeliveryTime,
73
73
  deliveryRate: stats.deliveryRate,
74
74
  bounceRate: stats.bounceRate,
75
75
  },
@@ -219,10 +219,10 @@ export class StatsHandler {
219
219
  metrics.email = {
220
220
  sent: stats.sentToday,
221
221
  received: stats.receivedToday,
222
- bounced: Math.floor(stats.sentToday * stats.bounceRate / 100),
222
+ bounced: stats.bouncedToday,
223
223
  queued: stats.queueSize,
224
- failed: 0,
225
- averageDeliveryTime: 0,
224
+ failed: stats.failedToday,
225
+ averageDeliveryTime: stats.averageDeliveryTime,
226
226
  deliveryRate: stats.deliveryRate,
227
227
  bounceRate: stats.bounceRate,
228
228
  timeSeries,
@@ -256,7 +256,15 @@ export class StatsHandler {
256
256
 
257
257
  if (sections.security && this.opsServerRef.dcRouterRef.metricsManager) {
258
258
  promises.push(
259
- this.opsServerRef.dcRouterRef.metricsManager.getSecurityStats().then(stats => {
259
+ Promise.all([
260
+ this.opsServerRef.dcRouterRef.metricsManager.getSecurityStats(),
261
+ this.opsServerRef.dcRouterRef.authenticationEventManager
262
+ ? this.opsServerRef.dcRouterRef.authenticationEventManager.getWindowStats()
263
+ : Promise.resolve({
264
+ windowStart: Date.now() - 86_400_000,
265
+ successes: 0, failures: 0, events: [],
266
+ }),
267
+ ]).then(([stats, authentication]) => {
260
268
  // Get recent events from the SecurityLogger singleton
261
269
  const securityLogger = SecurityLogger.getInstance();
262
270
  const recentEvents = securityLogger.getRecentEvents(50).map((evt) => ({
@@ -276,7 +284,10 @@ export class StatsHandler {
276
284
  spamDetected: stats.spamDetected,
277
285
  malwareDetected: stats.malwareDetected,
278
286
  phishingDetected: stats.phishingDetected,
279
- authenticationFailures: stats.authFailures,
287
+ authenticationFailures: authentication.failures,
288
+ authenticationSuccesses: authentication.successes,
289
+ authenticationWindowStart: authentication.windowStart,
290
+ authenticationEvents: authentication.events,
280
291
  suspiciousActivities: stats.totalThreatsBlocked,
281
292
  recentEvents,
282
293
  };
@@ -463,9 +474,12 @@ export class StatsHandler {
463
474
  sentToday: number;
464
475
  receivedToday: number;
465
476
  bounceRate: number;
477
+ failedToday: number;
478
+ bouncedToday: number;
466
479
  deliveryRate: number;
467
480
  queueSize: number;
468
481
  domainBreakdown?: { [domain: string]: interfaces.data.IEmailStats };
482
+ averageDeliveryTime: number;
469
483
  }> {
470
484
  // Get metrics from MetricsManager if available
471
485
  if (this.opsServerRef.dcRouterRef.metricsManager) {
@@ -474,8 +488,11 @@ export class StatsHandler {
474
488
  sentToday: emailStats.sentToday,
475
489
  receivedToday: emailStats.receivedToday,
476
490
  bounceRate: emailStats.bounceRate,
491
+ failedToday: emailStats.failedToday,
492
+ bouncedToday: emailStats.bouncedToday,
477
493
  deliveryRate: emailStats.deliveryRate,
478
494
  queueSize: emailStats.queueSize,
495
+ averageDeliveryTime: emailStats.averageDeliveryTime,
479
496
  };
480
497
  }
481
498
 
@@ -484,8 +501,11 @@ export class StatsHandler {
484
501
  sentToday: 0,
485
502
  receivedToday: 0,
486
503
  bounceRate: 0,
487
- deliveryRate: 100,
504
+ deliveryRate: 1,
505
+ failedToday: 0,
506
+ bouncedToday: 0,
488
507
  queueSize: 0,
508
+ averageDeliveryTime: 0,
489
509
  };
490
510
  }
491
511