@serve.zone/dcrouter 18.2.1 → 18.4.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.
Files changed (48) hide show
  1. package/deno.json +1 -1
  2. package/dist_serve/bundle.js +2986 -2954
  3. package/dist_ts/00_commitinfo_data.js +1 -1
  4. package/dist_ts/classes.dcrouter.js +4 -1
  5. package/dist_ts/db/documents/classes.cached.email.d.ts +61 -4
  6. package/dist_ts/db/documents/classes.cached.email.js +126 -9
  7. package/dist_ts/email/classes.accepted-email-spool.d.ts +1 -8
  8. package/dist_ts/email/classes.accepted-email-spool.js +18 -52
  9. package/dist_ts/email/classes.mail-egress-coordinator.js +10 -1
  10. package/dist_ts/email/helpers.email-log-traffic.d.ts +19 -0
  11. package/dist_ts/email/helpers.email-log-traffic.js +79 -0
  12. package/dist_ts/email/helpers.inbound-security.d.ts +8 -0
  13. package/dist_ts/email/helpers.inbound-security.js +35 -0
  14. package/dist_ts/email/index.d.ts +2 -0
  15. package/dist_ts/email/index.js +3 -1
  16. package/dist_ts/opsserver/handlers/email-ops.handler.d.ts +6 -0
  17. package/dist_ts/opsserver/handlers/email-ops.handler.js +162 -17
  18. package/dist_ts/remoteingress/classes.tunnel-manager.d.ts +4 -0
  19. package/dist_ts/remoteingress/classes.tunnel-manager.js +10 -1
  20. package/dist_ts_interfaces/requests/email-ops.d.ts +85 -0
  21. package/dist_ts_migrations/index.js +147 -14
  22. package/dist_ts_web/00_commitinfo_data.js +1 -1
  23. package/dist_ts_web/appstate/email-ops.d.ts +27 -1
  24. package/dist_ts_web/appstate/email-ops.js +100 -4
  25. package/dist_ts_web/appstate/runtime.js +11 -3
  26. package/dist_ts_web/elements/email/ops-view-email-security.d.ts +2 -0
  27. package/dist_ts_web/elements/email/ops-view-email-security.js +58 -16
  28. package/dist_ts_web/elements/email/ops-view-emails.d.ts +14 -2
  29. package/dist_ts_web/elements/email/ops-view-emails.js +128 -4
  30. package/package.json +6 -6
  31. package/readme.md +19 -0
  32. package/readme.plan.md +22 -0
  33. package/ts/00_commitinfo_data.ts +1 -1
  34. package/ts/classes.dcrouter.ts +7 -0
  35. package/ts/db/documents/classes.cached.email.ts +199 -7
  36. package/ts/email/classes.accepted-email-spool.ts +25 -66
  37. package/ts/email/classes.mail-egress-coordinator.ts +8 -0
  38. package/ts/email/helpers.email-log-traffic.ts +117 -0
  39. package/ts/email/helpers.inbound-security.ts +45 -0
  40. package/ts/email/index.ts +2 -0
  41. package/ts/opsserver/handlers/email-ops.handler.ts +214 -15
  42. package/ts/remoteingress/classes.tunnel-manager.ts +14 -0
  43. package/ts_web/00_commitinfo_data.ts +1 -1
  44. package/ts_web/appstate/email-ops.ts +129 -3
  45. package/ts_web/appstate/runtime.ts +13 -2
  46. package/ts_web/elements/email/ops-view-email-security.ts +56 -16
  47. package/ts_web/elements/email/ops-view-emails.ts +130 -2
  48. package/ts_web/readme.md +7 -0
@@ -32,7 +32,7 @@ var __runInitializers = (this && this.__runInitializers) || function (thisArg, i
32
32
  }
33
33
  return useValue ? value : void 0;
34
34
  };
35
- import { DeesElement, property, html, customElement, css, state, cssManager } from '@design.estate/dees-element';
35
+ import { DeesElement, html, customElement, css, state, cssManager } from '@design.estate/dees-element';
36
36
  import * as appstate from '../../appstate.js';
37
37
  import * as shared from '../shared/index.js';
38
38
  import * as interfaces from '../../../dist_ts_interfaces/index.js';
@@ -54,6 +54,15 @@ let OpsViewEmails = (() => {
54
54
  let _isLoading_decorators;
55
55
  let _isLoading_initializers = [];
56
56
  let _isLoading_extraInitializers = [];
57
+ let _traffic_decorators;
58
+ let _traffic_initializers = [];
59
+ let _traffic_extraInitializers = [];
60
+ let _searchQuery_decorators;
61
+ let _searchQuery_initializers = [];
62
+ let _searchQuery_extraInitializers = [];
63
+ let _selectedRange_decorators;
64
+ let _selectedRange_initializers = [];
65
+ let _selectedRange_extraInitializers = [];
57
66
  var OpsViewEmails = class extends _classSuper {
58
67
  static { _classThis = this; }
59
68
  static {
@@ -62,10 +71,16 @@ let OpsViewEmails = (() => {
62
71
  _selectedEmail_decorators = [state()];
63
72
  _currentView_decorators = [state()];
64
73
  _isLoading_decorators = [state()];
74
+ _traffic_decorators = [state()];
75
+ _searchQuery_decorators = [state()];
76
+ _selectedRange_decorators = [state()];
65
77
  __esDecorate(this, null, _emails_decorators, { kind: "accessor", name: "emails", static: false, private: false, access: { has: obj => "emails" in obj, get: obj => obj.emails, set: (obj, value) => { obj.emails = value; } }, metadata: _metadata }, _emails_initializers, _emails_extraInitializers);
66
78
  __esDecorate(this, null, _selectedEmail_decorators, { kind: "accessor", name: "selectedEmail", static: false, private: false, access: { has: obj => "selectedEmail" in obj, get: obj => obj.selectedEmail, set: (obj, value) => { obj.selectedEmail = value; } }, metadata: _metadata }, _selectedEmail_initializers, _selectedEmail_extraInitializers);
67
79
  __esDecorate(this, null, _currentView_decorators, { kind: "accessor", name: "currentView", static: false, private: false, access: { has: obj => "currentView" in obj, get: obj => obj.currentView, set: (obj, value) => { obj.currentView = value; } }, metadata: _metadata }, _currentView_initializers, _currentView_extraInitializers);
68
80
  __esDecorate(this, null, _isLoading_decorators, { kind: "accessor", name: "isLoading", static: false, private: false, access: { has: obj => "isLoading" in obj, get: obj => obj.isLoading, set: (obj, value) => { obj.isLoading = value; } }, metadata: _metadata }, _isLoading_initializers, _isLoading_extraInitializers);
81
+ __esDecorate(this, null, _traffic_decorators, { kind: "accessor", name: "traffic", static: false, private: false, access: { has: obj => "traffic" in obj, get: obj => obj.traffic, set: (obj, value) => { obj.traffic = value; } }, metadata: _metadata }, _traffic_initializers, _traffic_extraInitializers);
82
+ __esDecorate(this, null, _searchQuery_decorators, { kind: "accessor", name: "searchQuery", static: false, private: false, access: { has: obj => "searchQuery" in obj, get: obj => obj.searchQuery, set: (obj, value) => { obj.searchQuery = value; } }, metadata: _metadata }, _searchQuery_initializers, _searchQuery_extraInitializers);
83
+ __esDecorate(this, null, _selectedRange_decorators, { kind: "accessor", name: "selectedRange", static: false, private: false, access: { has: obj => "selectedRange" in obj, get: obj => obj.selectedRange, set: (obj, value) => { obj.selectedRange = value; } }, metadata: _metadata }, _selectedRange_initializers, _selectedRange_extraInitializers);
69
84
  __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
70
85
  OpsViewEmails = _classThis = _classDescriptor.value;
71
86
  if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
@@ -82,13 +97,26 @@ let OpsViewEmails = (() => {
82
97
  #isLoading_accessor_storage = (__runInitializers(this, _currentView_extraInitializers), __runInitializers(this, _isLoading_initializers, false));
83
98
  get isLoading() { return this.#isLoading_accessor_storage; }
84
99
  set isLoading(value) { this.#isLoading_accessor_storage = value; }
85
- stateSubscription = __runInitializers(this, _isLoading_extraInitializers);
100
+ #traffic_accessor_storage = (__runInitializers(this, _isLoading_extraInitializers), __runInitializers(this, _traffic_initializers, null));
101
+ get traffic() { return this.#traffic_accessor_storage; }
102
+ set traffic(value) { this.#traffic_accessor_storage = value; }
103
+ #searchQuery_accessor_storage = (__runInitializers(this, _traffic_extraInitializers), __runInitializers(this, _searchQuery_initializers, ''));
104
+ get searchQuery() { return this.#searchQuery_accessor_storage; }
105
+ set searchQuery(value) { this.#searchQuery_accessor_storage = value; }
106
+ #selectedRange_accessor_storage = (__runInitializers(this, _searchQuery_extraInitializers), __runInitializers(this, _selectedRange_initializers, null));
107
+ get selectedRange() { return this.#selectedRange_accessor_storage; }
108
+ set selectedRange(value) { this.#selectedRange_accessor_storage = value; }
109
+ stateSubscription = __runInitializers(this, _selectedRange_extraInitializers);
86
110
  lastEmailOpsUpdate = 0;
87
111
  detailRequestGeneration = 0;
112
+ searchDebounceTimer;
88
113
  async connectedCallback() {
89
114
  await super.connectedCallback();
90
115
  this.stateSubscription = appstate.emailOpsStatePart.select().subscribe((state) => {
91
116
  this.emails = state.emails;
117
+ this.traffic = state.traffic;
118
+ this.searchQuery = state.searchQuery;
119
+ this.selectedRange = state.selectedRange;
92
120
  const hasNewSnapshot = state.lastUpdated > this.lastEmailOpsUpdate;
93
121
  this.lastEmailOpsUpdate = Math.max(this.lastEmailOpsUpdate, state.lastUpdated);
94
122
  if (hasNewSnapshot && this.selectedEmail) {
@@ -104,6 +132,10 @@ let OpsViewEmails = (() => {
104
132
  if (this.stateSubscription) {
105
133
  this.stateSubscription.unsubscribe();
106
134
  }
135
+ if (this.searchDebounceTimer !== undefined) {
136
+ window.clearTimeout(this.searchDebounceTimer);
137
+ this.searchDebounceTimer = undefined;
138
+ }
107
139
  }
108
140
  static styles = [
109
141
  cssManager.defaultStyles,
@@ -117,6 +149,26 @@ let OpsViewEmails = (() => {
117
149
  .viewContainer {
118
150
  height: 100%;
119
151
  }
152
+
153
+ .chartStack {
154
+ display: flex;
155
+ flex-direction: column;
156
+ gap: 8px;
157
+ }
158
+
159
+ .rangeSummary {
160
+ display: flex;
161
+ align-items: center;
162
+ justify-content: space-between;
163
+ min-height: 32px;
164
+ padding: 0 4px;
165
+ color: ${cssManager.bdTheme('#374151', '#d1d5db')};
166
+ font-size: 13px;
167
+ }
168
+
169
+ .chartStack dees-chart-area {
170
+ min-height: 360px;
171
+ }
120
172
  `,
121
173
  ];
122
174
  render() {
@@ -133,8 +185,34 @@ let OpsViewEmails = (() => {
133
185
  : html `
134
186
  <sz-mta-list-view
135
187
  .emails=${this.emails}
188
+ .searchQuery=${this.searchQuery}
189
+ .stickyFilterBar=${true}
190
+ @search-change=${this.handleSearchChange}
136
191
  @email-click=${this.handleEmailClick}
137
- ></sz-mta-list-view>
192
+ >
193
+ ${this.traffic ? html `
194
+ <div slot="above-table" class="chartStack">
195
+ ${this.selectedRange ? html `
196
+ <div class="rangeSummary">
197
+ <span>${this.formatSelectedRange()}</span>
198
+ <dees-button @click=${this.clearSelectedRange}>Clear time range</dees-button>
199
+ </div>
200
+ ` : html ``}
201
+ <dees-chart-area
202
+ .label=${this.traffic.label}
203
+ .series=${this.getEmailTrafficSeries()}
204
+ .realtimeMode=${true}
205
+ .rollingWindow=${this.traffic.durationMs}
206
+ .legendStats=${['latest']}
207
+ .chartLines=${[]}
208
+ .rangeSelectionEnabled=${true}
209
+ .selectedRange=${this.selectedRange}
210
+ .yAxisFormatter=${(valueArg) => `${valueArg}`}
211
+ @range-change=${this.handleRangeChange}
212
+ ></dees-chart-area>
213
+ </div>
214
+ ` : html ``}
215
+ </sz-mta-list-view>
138
216
  `}
139
217
  </div>
140
218
  `;
@@ -162,6 +240,52 @@ let OpsViewEmails = (() => {
162
240
  async handleEmailClick(e) {
163
241
  await this.refreshEmailDetail(e.detail.id, true);
164
242
  }
243
+ handleSearchChange(eventArg) {
244
+ const searchQuery = appstate.setActiveEmailSearchQuery(eventArg.detail.searchQuery);
245
+ if (this.searchDebounceTimer !== undefined) {
246
+ window.clearTimeout(this.searchDebounceTimer);
247
+ }
248
+ this.searchDebounceTimer = window.setTimeout(() => {
249
+ this.searchDebounceTimer = undefined;
250
+ void appstate.emailOpsStatePart.dispatchAction(appstate.fetchAllEmailsAction, { searchQuery });
251
+ }, 300);
252
+ }
253
+ handleRangeChange = (eventArg) => {
254
+ void appstate.emailOpsStatePart.dispatchAction(appstate.fetchAllEmailsAction, { selectedRange: eventArg.detail });
255
+ };
256
+ clearSelectedRange = () => {
257
+ void appstate.emailOpsStatePart.dispatchAction(appstate.fetchAllEmailsAction, { selectedRange: null });
258
+ };
259
+ formatSelectedRange() {
260
+ if (!this.selectedRange)
261
+ return '';
262
+ const formatter = new Intl.DateTimeFormat(undefined, {
263
+ dateStyle: 'medium',
264
+ timeStyle: 'short',
265
+ });
266
+ return `Showing emails from ${formatter.format(this.selectedRange.from)} to ${formatter.format(this.selectedRange.to)}`;
267
+ }
268
+ getEmailTrafficSeries() {
269
+ if (!this.traffic)
270
+ return [];
271
+ return [
272
+ {
273
+ name: 'Sent',
274
+ color: '#22c55e',
275
+ data: this.traffic.sent.map((point) => ({ x: point.timestamp, y: point.value })),
276
+ },
277
+ {
278
+ name: 'Received',
279
+ color: '#3b82f6',
280
+ data: this.traffic.received.map((point) => ({ x: point.timestamp, y: point.value })),
281
+ },
282
+ {
283
+ name: 'Failed',
284
+ color: '#ef4444',
285
+ data: this.traffic.failed.map((point) => ({ x: point.timestamp, y: point.value })),
286
+ },
287
+ ];
288
+ }
165
289
  handleBack() {
166
290
  this.detailRequestGeneration++;
167
291
  this.selectedEmail = null;
@@ -174,4 +298,4 @@ let OpsViewEmails = (() => {
174
298
  return OpsViewEmails = _classThis;
175
299
  })();
176
300
  export { OpsViewEmails };
177
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3BzLXZpZXctZW1haWxzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vdHNfd2ViL2VsZW1lbnRzL2VtYWlsL29wcy12aWV3LWVtYWlscy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0FBQUEsT0FBTyxFQUFFLFdBQVcsRUFBRSxRQUFRLEVBQUUsSUFBSSxFQUFFLGFBQWEsRUFBdUIsR0FBRyxFQUFFLEtBQUssRUFBRSxVQUFVLEVBQUUsTUFBTSw2QkFBNkIsQ0FBQztBQUN0SSxPQUFPLEtBQUssUUFBUSxNQUFNLG1CQUFtQixDQUFDO0FBQzlDLE9BQU8sS0FBSyxNQUFNLE1BQU0sb0JBQW9CLENBQUM7QUFDN0MsT0FBTyxLQUFLLFVBQVUsTUFBTSxzQ0FBc0MsQ0FBQztJQVN0RCxhQUFhOzRCQUR6QixhQUFhLENBQUMsaUJBQWlCLENBQUM7Ozs7c0JBQ0UsV0FBVzs7Ozs7Ozs7Ozs7Ozs2QkFBbkIsU0FBUSxXQUFXOzs7O2tDQUMzQyxLQUFLLEVBQUU7eUNBR1AsS0FBSyxFQUFFO3VDQUdQLEtBQUssRUFBRTtxQ0FHUCxLQUFLLEVBQUU7WUFSUix1S0FBUyxNQUFNLDZCQUFOLE1BQU0sdUZBQW9DO1lBR25ELDRMQUFTLGFBQWEsNkJBQWIsYUFBYSxxR0FBaUQ7WUFHdkUsc0xBQVMsV0FBVyw2QkFBWCxXQUFXLGlHQUE2QjtZQUdqRCxnTEFBUyxTQUFTLDZCQUFULFNBQVMsNkZBQVM7WUFYN0IsNktBdUdDOzs7O1FBckdDLHlFQUFnRCxFQUFFLEVBQUM7UUFBbkQsSUFBUyxNQUFNLDRDQUFvQztRQUFuRCxJQUFTLE1BQU0sa0RBQW9DO1FBR25ELDRJQUFrRSxJQUFJLEdBQUM7UUFBdkUsSUFBUyxhQUFhLG1EQUFpRDtRQUF2RSxJQUFTLGFBQWEseURBQWlEO1FBR3ZFLCtJQUEwQyxNQUFNLEdBQUM7UUFBakQsSUFBUyxXQUFXLGlEQUE2QjtRQUFqRCxJQUFTLFdB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
301
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3BzLXZpZXctZW1haWxzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vdHNfd2ViL2VsZW1lbnRzL2VtYWlsL29wcy12aWV3LWVtYWlscy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0FBQUEsT0FBTyxFQUFFLFdBQVcsRUFBRSxJQUFJLEVBQUUsYUFBYSxFQUFFLEdBQUcsRUFBRSxLQUFLLEVBQUUsVUFBVSxFQUFFLE1BQU0sNkJBQTZCLENBQUM7QUFDdkcsT0FBTyxLQUFLLFFBQVEsTUFBTSxtQkFBbUIsQ0FBQztBQUM5QyxPQUFPLEtBQUssTUFBTSxNQUFNLG9CQUFvQixDQUFDO0FBQzdDLE9BQU8sS0FBSyxVQUFVLE1BQU0sc0NBQXNDLENBQUM7SUFTdEQsYUFBYTs0QkFEekIsYUFBYSxDQUFDLGlCQUFpQixDQUFDOzs7O3NCQUNFLFdBQVc7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7NkJBQW5CLFNBQVEsV0FBVzs7OztrQ0FDM0MsS0FBSyxFQUFFO3lDQUdQLEtBQUssRUFBRTt1Q0FHUCxLQUFLLEVBQUU7cUNBR1AsS0FBSyxFQUFFO21DQUdQLEtBQUssRUFBRTt1Q0FHUCxLQUFLLEVBQUU7eUNBR1AsS0FBSyxFQUFFO1lBakJSLHVLQUFTLE1BQU0sNkJBQU4sTUFBTSx1RkFBb0M7WUFHbkQsNExBQVMsYUFBYSw2QkFBYixhQUFhLHFHQUFpRDtZQUd2RSxzTEFBUyxXQUFXLDZCQUFYLFdBQVcsaUdBQTZCO1lBR2pELGdMQUFTLFNBQVMsNkJBQVQsU0FBUyw2RkFBUztZQUczQiwwS0FBUyxPQUFPLDZCQUFQLE9BQU8seUZBQXFEO1lBR3JFLHNMQUFTLFdBQVcsNkJBQVgsV0FBVyxpR0FBTTtZQUcxQiw0TEFBUyxhQUFhLDZCQUFiLGFBQWEscUdBQTZDO1lBcEJyRSw2S0F1T0M7Ozs7UUFyT0MseUVBQWdELEVBQUUsRUFBQztRQUFuRCxJQUFTLE1BQU0sNENBQW9D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
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@serve.zone/dcrouter",
3
3
  "private": false,
4
- "version": "18.2.1",
4
+ "version": "18.4.0",
5
5
  "description": "A multifaceted routing service handling mail and SMS delivery functions.",
6
6
  "type": "module",
7
7
  "bin": {
@@ -20,7 +20,7 @@
20
20
  "@git.zone/tsdeno": "^1.7.0",
21
21
  "@git.zone/tsdocker": "^3.1.0",
22
22
  "@git.zone/tsrun": "^2.0.5",
23
- "@git.zone/tstest": "^3.6.6",
23
+ "@git.zone/tstest": "^3.6.7",
24
24
  "@git.zone/tswatch": "^3.3.5",
25
25
  "@types/node": "26.1.1",
26
26
  "@types/web-push": "3.6.4"
@@ -31,7 +31,7 @@
31
31
  "@api.global/typedserver": "^8.6.4",
32
32
  "@api.global/typedsocket": "^5.0.0",
33
33
  "@apiclient.xyz/cloudflare": "^7.1.0",
34
- "@design.estate/dees-catalog": "^3.98.1",
34
+ "@design.estate/dees-catalog": "^3.99.0",
35
35
  "@design.estate/dees-element": "^2.4.0",
36
36
  "@idp.global/sdk": "^14.3.0",
37
37
  "@push.rocks/lik": "^6.4.1",
@@ -50,7 +50,7 @@
50
50
  "@push.rocks/smartmcp": "^0.3.0",
51
51
  "@push.rocks/smartmetrics": "^3.0.3",
52
52
  "@push.rocks/smartmigration": "1.4.1",
53
- "@push.rocks/smartmta": "^9.3.0",
53
+ "@push.rocks/smartmta": "^9.4.0",
54
54
  "@push.rocks/smartnetwork": "^4.8.0",
55
55
  "@push.rocks/smartpath": "^6.0.0",
56
56
  "@push.rocks/smartpromise": "^4.2.4",
@@ -63,9 +63,9 @@
63
63
  "@push.rocks/smartunique": "^3.0.10",
64
64
  "@push.rocks/smartvpn": "1.22.0",
65
65
  "@push.rocks/taskbuffer": "^8.0.2",
66
- "@serve.zone/catalog": "^2.19.0",
66
+ "@serve.zone/catalog": "^2.21.0",
67
67
  "@serve.zone/interfaces": "^19.2.0",
68
- "@serve.zone/remoteingress": "^5.0.3",
68
+ "@serve.zone/remoteingress": "^5.1.0",
69
69
  "@tsclass/tsclass": "^9.5.1",
70
70
  "@types/qrcode": "^1.5.6",
71
71
  "ipaddr.js": "^2.4.0",
package/readme.md CHANGED
@@ -474,6 +474,25 @@ Use `@serve.zone/dcrouter/interfaces` or `@serve.zone/dcrouter-interfaces` when
474
474
 
475
475
  Gateway-client mail contracts let Cloudly or Onebox claim exact app addresses, attach inbound `smtpForward` targets, enable managed outbound SMTP credentials, rotate those credentials, enqueue service mail, and query delivery status through TypedRequest. Managed SMTP users can only send as their exact claimed address; mismatched envelope or header `From` values are rejected before relay. Delivery status queries return the dcrouter spool item state, including deferred SmartMTA errors and next retry timestamps when outbound delivery is temporarily delayed.
476
476
 
477
+ ### Email Operations Views
478
+
479
+ The Email Log keeps its search controls sticky and renders the traffic chart
480
+ above the message table. With no query, the chart covers the latest 24 hours.
481
+ Searches expand the chart window to the smallest supported period that contains
482
+ matching retained messages (7, 14, or 30 days). Dragging across the chart
483
+ selects an epoch-millisecond time range for the table while the chart continues
484
+ to show the complete search context; clearing the selection restores the full
485
+ result list.
486
+
487
+ Inbound messages persist a separate security disposition:
488
+ `accepted`, `flagged`, or `rejected`. SPF, DKIM, or DMARC findings therefore
489
+ remain visible in Email Security without overwriting the transport/delivery
490
+ status. The security table shows time, sender, recipients, subject, and the
491
+ specific authentication failure. Sent-message detail exposes the asynchronous
492
+ outbound authentication self-check based on the exact post-DKIM bytes and the
493
+ actual RemoteIngress source address. It is diagnostic evidence, not a receipt
494
+ from the destination server.
495
+
477
496
  ### RemoteIngress SMTP Egress
478
497
 
479
498
  The email settings API and Ops UI expose `outboundMode`. Keep it at `direct` to let SmartMTA dial destination MX hosts from the dcrouter hub. Set it to `remoteIngress` to make SmartMTA request a one-shot RemoteIngress TCP egress proxy for each outbound SMTP connection while keeping the logical MX host and port for SMTP/TLS identity.
package/readme.plan.md ADDED
@@ -0,0 +1,22 @@
1
+ # Platform refactoring program
2
+
3
+ Status: approved 2026-07-21, derived from that day's incident ledger (deploy-verification crashes, unbounded scans, DNS/IP logic duplication, version-skew scares). Upstream foundations are RELEASED; consumer adoption and the dcrouter-internal refactors are sequenced below — do not start them mid-wave, fold them into the next natural release of each surface.
4
+
5
+ ## Shipped upstream (adopt on next dependency bump)
6
+
7
+ 1. **Typed-request error boundary — already existed.** @api.global/typedrequest ≥ 3.3.2 sanitizes every handler/middleware throw into a structured error envelope (no socket destruction). Action: ensure every deployed consumer (cloudly, dcrouter, coreflow, onebox) resolves ≥ 3.3.2 and remove any local try/catch layers that only re-implement it.
8
+ 2. **Canonical IP helpers — @push.rocks/smartnetwork 4.9.0.** `isPublicIpAddress`, `toReverseDnsName` (non-throwing), `expandIpv6`, alongside the existing `getIpVersion`/`isValidIpv4`/`isValidIpv6`. Adoption: dcrouter ts/email/classes.mail-dns-sync.ts drops its private `toReverseDnsName`/`expandIpv6` (and the `Invalid IP address:` throw); cloudly's `isPublicDeploymentAddress` (classes.deploymentoperationmanager.ts) delegates to `isPublicIpAddress` (IPv4 semantics are byte-identical; IPv6 is 2000::/3 minus 2001:db8::/32).
9
+ 3. **Cursor pagination as the org default — @push.rocks/smartdata 7.4.0.** `SmartDataDbDoc.getPagedInstances({ filter, sortField, sortDirection, uniqueField, limit ≤ 1000, cursor, withTotal })` — seek pagination with a unique tiebreaker, never materializes the full set. Adoption: OpsConfigEventDoc.findFiltered becomes a thin wrapper; every future list surface uses it. Review rule: a list API without a cursor is a defect.
10
+ 4. **Binary provenance — @git.zone/tsrust 1.7.0.** Every built binary gets a JSON trailer (project, version, git commit, target, builtAt, tsrust version); `tsrust inspect <binary>` prints it. Adoption: bump tsrust across the Rust-bridge repos (smartdb, smartproxy, smartmta, smartnetwork, coretraffic, remoteingress) on their next releases; deployment verification can then assert binary identity instead of guessing (see the strings-on-x86 pitfall in smartdb readme.hints.md).
11
+
12
+ ## Sequenced (do NOT start mid-wave)
13
+
14
+ 5. **Ops-handler base class (dcrouter).** 25 handler files carry 11 copies of the requireAuth wrapper and 49 hand-written "manager not initialized" guards. Introduce a base with `registerMethod(name, scope, handlerFn)` doing auth, manager guard, error boundary, and realtime invalidation; migrate handlers opportunistically (new handlers must use it; old ones migrate when touched).
15
+ 6. **Composition-root service table (dcrouter).** classes.dcrouter.ts (2,326 lines) has 20 near-identical addService blocks with drift (withRetry presence, error logging). Replace with a declarative registration table; behavior-preserving.
16
+ 7. **Mail-detail contract dedup (interfaces + catalog + dcrouter).** IEmailDetail and friends exist twice by convention ("matches @serve.zone/catalog"). Move the contract into @serve.zone/interfaces; catalog and dcrouter consume it. Sequence AFTER the in-flight mail-detail wave (countdown, verdicts, transfer stats) fully lands — one migration wave, no compatibility layer.
17
+ 8. **Version-skew hardening (conventions).** (a) Tolerant-reader parsing for persisted operation/evidence/state documents that cross release boundaries; (b) version handshake wherever a live process or socket is reused across upgrades (LocalSmartDb engine reuse — smartdb plan Track 6; gateway-client policyGeneration is the precedent).
18
+
19
+ ## Non-goals
20
+
21
+ - No big-bang rewrites: every item lands either additive-upstream or inside a surface's next natural release.
22
+ - No compatibility layers (org rule): consumers move to the canonical helpers outright when their wave touches the code.
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '18.2.1',
6
+ version: '18.4.0',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -2142,6 +2142,13 @@ export class DcRouter {
2142
2142
  await this.acceptedEmailSpool.trackSmtpTransaction(transaction, emailServer);
2143
2143
  },
2144
2144
  );
2145
+ this.addEmailEventSubscription(
2146
+ emailServer.deliverySystem,
2147
+ 'smtpTransactionUpdated',
2148
+ async (transaction: import('./db/documents/classes.cached.email.js').ICachedEmailSmtpTransaction) => {
2149
+ await this.acceptedEmailSpool.trackSmtpTransaction(transaction, emailServer);
2150
+ },
2151
+ );
2145
2152
 
2146
2153
  // Wire delivery events to MetricsManager and logger using smartmta's public queue APIs.
2147
2154
  if (this.metricsManager) {
@@ -2,6 +2,7 @@ import * as plugins from '../../plugins.js';
2
2
  import { DcRouterDb } from '../classes.dcrouter-db.js';
3
3
 
4
4
  const TTL = plugins.smartdata.smartdataTtlValues;
5
+ const DB_OPERATION_TIMEOUT_MS = 5_000;
5
6
 
6
7
  /**
7
8
  * Email status in the cache
@@ -13,6 +14,22 @@ export type TCachedEmailStatus = 'pending' | 'processing' | 'queued' | 'delivere
13
14
  * 'outbound' = locally submitted for delivery.
14
15
  */
15
16
  export type TCachedEmailDirection = 'inbound' | 'outbound';
17
+ export type TCachedEmailSecurityDisposition = 'accepted' | 'flagged' | 'rejected';
18
+
19
+ export interface ICachedEmailListFilter {
20
+ direction?: TCachedEmailDirection;
21
+ recipientDomain?: string;
22
+ search?: string;
23
+ from?: number;
24
+ to?: number;
25
+ }
26
+
27
+ export interface ICachedEmailTrafficBucket {
28
+ bucketStart: number;
29
+ sent: number;
30
+ received: number;
31
+ failed: number;
32
+ }
16
33
 
17
34
  export type TCachedEmailSmtpDeliveryPhase =
18
35
  | 'connect'
@@ -53,6 +70,7 @@ export interface ICachedEmailSmtpTransaction {
53
70
  }>;
54
71
  failurePhase?: TCachedEmailSmtpDeliveryPhase;
55
72
  error?: string;
73
+ outboundAuthentication?: ICachedEmailOutboundAuthenticationCheck;
56
74
  transcript: Array<{
57
75
  timestampMs: number;
58
76
  phase: TCachedEmailSmtpDeliveryPhase;
@@ -62,6 +80,40 @@ export interface ICachedEmailSmtpTransaction {
62
80
  }>;
63
81
  }
64
82
 
83
+ export interface ICachedEmailOutboundAuthenticationCheck {
84
+ state: 'pending' | 'completed' | 'indeterminate' | 'not-evaluated';
85
+ overall: 'pass' | 'fail' | 'indeterminate';
86
+ checkedAt: string;
87
+ signedMessageSha256?: string;
88
+ error?: string;
89
+ identity?: {
90
+ sourceIp: string;
91
+ addressFamily: 'ipv4' | 'ipv6';
92
+ heloDomain: string;
93
+ receiverHostname: string;
94
+ envelopeFrom: string;
95
+ dkimDomain?: string;
96
+ dkimSelector?: string;
97
+ };
98
+ dkim?: Array<{
99
+ is_valid: boolean;
100
+ domain: string | null;
101
+ selector: string | null;
102
+ status: string;
103
+ details: string | null;
104
+ }>;
105
+ spf?: { result: string; domain: string; ip: string; explanation: string | null } | null;
106
+ dmarc?: {
107
+ passed: boolean;
108
+ policy: string;
109
+ domain: string;
110
+ dkim_result: string;
111
+ spf_result: string;
112
+ action: string;
113
+ details: string | null;
114
+ } | null;
115
+ }
116
+
65
117
  /**
66
118
  * Helper to get the smartdata database instance
67
119
  */
@@ -161,6 +213,11 @@ export class CachedEmail extends plugins.smartdata.SmartdataCachedDocument<Cache
161
213
  @plugins.smartdata.svDb()
162
214
  public inboundSecurityResults?: string;
163
215
 
216
+ /** Authentication disposition, kept separate from delivery/lifecycle status. */
217
+ @plugins.smartdata.index()
218
+ @plugins.smartdata.svDb()
219
+ public inboundSecurityDisposition?: TCachedEmailSecurityDisposition;
220
+
164
221
  /**
165
222
  * Number of delivery attempts
166
223
  */
@@ -222,6 +279,7 @@ export class CachedEmail extends plugins.smartdata.SmartdataCachedDocument<Cache
222
279
  /**
223
280
  * Epoch ms when the message was accepted (for newest-first listings)
224
281
  */
282
+ @plugins.smartdata.index()
225
283
  @plugins.smartdata.svDb()
226
284
  public acceptedAt: number = 0;
227
285
 
@@ -276,6 +334,9 @@ export class CachedEmail extends plugins.smartdata.SmartdataCachedDocument<Cache
276
334
  if (this.rawContentObjectKey) {
277
335
  delete savableObject.rawContent;
278
336
  }
337
+ if (this.inboundSecurityDisposition === undefined) {
338
+ delete savableObject.inboundSecurityDisposition;
339
+ }
279
340
  return savableObject;
280
341
  }
281
342
 
@@ -366,21 +427,152 @@ export class CachedEmail extends plugins.smartdata.SmartdataCachedDocument<Cache
366
427
  }
367
428
  }
368
429
 
430
+ private static getNativeCollection() {
431
+ return getDb().mongoDb.collection('CachedEmail');
432
+ }
433
+
434
+ private static buildListQuery(filterArg: ICachedEmailListFilter): Record<string, any> {
435
+ const query: Record<string, any> = {};
436
+ if (filterArg.direction) {
437
+ query.direction = filterArg.direction;
438
+ }
439
+ if (filterArg.recipientDomain) {
440
+ query.recipientDomains = filterArg.recipientDomain.toLowerCase();
441
+ }
442
+ if (filterArg.search) {
443
+ if (typeof filterArg.search !== 'string' || filterArg.search.length > 200) {
444
+ throw new Error('Invalid CachedEmail search');
445
+ }
446
+ const escapedSearch = filterArg.search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
447
+ query.$or = [
448
+ { from: { $regex: escapedSearch, $options: 'i' } },
449
+ { to: { $regex: escapedSearch, $options: 'i' } },
450
+ { subject: { $regex: escapedSearch, $options: 'i' } },
451
+ { messageId: { $regex: escapedSearch, $options: 'i' } },
452
+ ];
453
+ }
454
+ if (filterArg.from !== undefined || filterArg.to !== undefined) {
455
+ const from = filterArg.from;
456
+ const to = filterArg.to;
457
+ if (
458
+ !Number.isSafeInteger(from)
459
+ || !Number.isSafeInteger(to)
460
+ || (from as number) < 0
461
+ || (to as number) < (from as number)
462
+ ) {
463
+ throw new Error('Invalid CachedEmail time range');
464
+ }
465
+ query.acceptedAt = { $gte: from, $lte: to };
466
+ }
467
+ return query;
468
+ }
469
+
369
470
  /**
370
471
  * Find the most recently accepted emails, newest first.
371
472
  */
372
473
  public static async findRecent(
373
474
  limit = 200,
374
- filter: { direction?: TCachedEmailDirection; recipientDomain?: string } = {},
475
+ filter: ICachedEmailListFilter = {},
375
476
  ): Promise<CachedEmail[]> {
376
- const query: Record<string, any> = {};
377
- if (filter.direction) {
378
- query.direction = filter.direction;
477
+ const query = CachedEmail.buildListQuery(filter);
478
+ return await CachedEmail.findLimited(query, limit, { acceptedAt: -1 });
479
+ }
480
+
481
+ public static async findOldestAcceptedAt(
482
+ filterArg: ICachedEmailListFilter,
483
+ cutoffArg: number,
484
+ ): Promise<number | null> {
485
+ if (!Number.isSafeInteger(cutoffArg) || cutoffArg < 0) {
486
+ throw new Error('CachedEmail.findOldestAcceptedAt requires a valid cutoff');
487
+ }
488
+ const query = CachedEmail.buildListQuery(filterArg);
489
+ query.acceptedAt = { $gte: cutoffArg };
490
+ const collection = await CachedEmail.getNativeCollection();
491
+ const row = await collection.findOne(
492
+ query,
493
+ {
494
+ projection: { acceptedAt: 1 },
495
+ sort: { acceptedAt: 1 },
496
+ timeoutMS: DB_OPERATION_TIMEOUT_MS,
497
+ },
498
+ );
499
+ const acceptedAt = Number(row?.acceptedAt);
500
+ return Number.isSafeInteger(acceptedAt) && acceptedAt >= 0 ? acceptedAt : null;
501
+ }
502
+
503
+ public static async aggregateEmailLogTraffic(
504
+ filterArg: ICachedEmailListFilter,
505
+ windowStartArg: number,
506
+ windowEndArg: number,
507
+ bucketSizeMsArg: number,
508
+ ): Promise<ICachedEmailTrafficBucket[]> {
509
+ if (
510
+ !Number.isSafeInteger(windowStartArg)
511
+ || !Number.isSafeInteger(windowEndArg)
512
+ || !Number.isSafeInteger(bucketSizeMsArg)
513
+ || windowStartArg < 0
514
+ || windowEndArg < windowStartArg
515
+ || bucketSizeMsArg <= 0
516
+ ) {
517
+ throw new Error('CachedEmail.aggregateEmailLogTraffic received an invalid window');
379
518
  }
380
- if (filter.recipientDomain) {
381
- query.recipientDomains = filter.recipientDomain.toLowerCase();
519
+ const query = CachedEmail.buildListQuery(filterArg);
520
+ query.acceptedAt = { $gte: windowStartArg, $lte: windowEndArg };
521
+ const collection = await CachedEmail.getNativeCollection();
522
+ const cursor = collection.find(
523
+ query,
524
+ {
525
+ projection: { acceptedAt: 1, direction: 1, status: 1 },
526
+ batchSize: 1000,
527
+ timeoutMS: DB_OPERATION_TIMEOUT_MS,
528
+ },
529
+ );
530
+ try {
531
+ const buckets = new Map<number, ICachedEmailTrafficBucket>();
532
+ while (await cursor.hasNext()) {
533
+ const row = await cursor.next();
534
+ if (!row) {
535
+ continue;
536
+ }
537
+ const acceptedAt = Number(row.acceptedAt);
538
+ if (!Number.isSafeInteger(acceptedAt)) {
539
+ continue;
540
+ }
541
+ const bucketStart = Math.floor(acceptedAt / bucketSizeMsArg) * bucketSizeMsArg;
542
+ let bucket = buckets.get(bucketStart);
543
+ if (!bucket) {
544
+ bucket = { bucketStart, sent: 0, received: 0, failed: 0 };
545
+ buckets.set(bucketStart, bucket);
546
+ }
547
+ if (row.status === 'failed' || row.status === 'rejected') {
548
+ bucket.failed++;
549
+ } else if (row.direction === 'outbound') {
550
+ bucket.sent++;
551
+ } else if (row.direction === 'inbound') {
552
+ bucket.received++;
553
+ }
554
+ }
555
+ return [...buckets.values()].sort((firstArg, secondArg) => (
556
+ firstArg.bucketStart - secondArg.bucketStart
557
+ ));
558
+ } finally {
559
+ await cursor.close({ timeoutMS: DB_OPERATION_TIMEOUT_MS });
382
560
  }
383
- return await CachedEmail.findLimited(query, limit, { acceptedAt: -1 });
561
+ }
562
+
563
+ public static async findRecentSecurityCandidates(
564
+ limitArg: number,
565
+ cutoffArg: number,
566
+ ): Promise<CachedEmail[]> {
567
+ const safeLimit = Math.min(Math.max(Math.floor(limitArg), 1), 1000);
568
+ if (!Number.isSafeInteger(cutoffArg) || cutoffArg < 0) {
569
+ throw new Error('CachedEmail.findRecentSecurityCandidates requires a valid cutoff');
570
+ }
571
+ return await CachedEmail.findLimited({
572
+ direction: 'inbound',
573
+ inboundSecurityDisposition: { $in: ['flagged', 'rejected'] },
574
+ acceptedAt: { $gte: cutoffArg },
575
+ }, safeLimit, { acceptedAt: -1 });
384
576
  }
385
577
 
386
578
  /**