@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.
- package/deno.json +1 -1
- package/dist_serve/bundle.js +2986 -2954
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.js +4 -1
- package/dist_ts/db/documents/classes.cached.email.d.ts +61 -4
- package/dist_ts/db/documents/classes.cached.email.js +126 -9
- package/dist_ts/email/classes.accepted-email-spool.d.ts +1 -8
- package/dist_ts/email/classes.accepted-email-spool.js +18 -52
- package/dist_ts/email/classes.mail-egress-coordinator.js +10 -1
- package/dist_ts/email/helpers.email-log-traffic.d.ts +19 -0
- package/dist_ts/email/helpers.email-log-traffic.js +79 -0
- package/dist_ts/email/helpers.inbound-security.d.ts +8 -0
- package/dist_ts/email/helpers.inbound-security.js +35 -0
- package/dist_ts/email/index.d.ts +2 -0
- package/dist_ts/email/index.js +3 -1
- package/dist_ts/opsserver/handlers/email-ops.handler.d.ts +6 -0
- package/dist_ts/opsserver/handlers/email-ops.handler.js +162 -17
- package/dist_ts/remoteingress/classes.tunnel-manager.d.ts +4 -0
- package/dist_ts/remoteingress/classes.tunnel-manager.js +10 -1
- package/dist_ts_interfaces/requests/email-ops.d.ts +85 -0
- package/dist_ts_migrations/index.js +147 -14
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate/email-ops.d.ts +27 -1
- package/dist_ts_web/appstate/email-ops.js +100 -4
- package/dist_ts_web/appstate/runtime.js +11 -3
- package/dist_ts_web/elements/email/ops-view-email-security.d.ts +2 -0
- package/dist_ts_web/elements/email/ops-view-email-security.js +58 -16
- package/dist_ts_web/elements/email/ops-view-emails.d.ts +14 -2
- package/dist_ts_web/elements/email/ops-view-emails.js +128 -4
- package/package.json +6 -6
- package/readme.md +19 -0
- package/readme.plan.md +22 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +7 -0
- package/ts/db/documents/classes.cached.email.ts +199 -7
- package/ts/email/classes.accepted-email-spool.ts +25 -66
- package/ts/email/classes.mail-egress-coordinator.ts +8 -0
- package/ts/email/helpers.email-log-traffic.ts +117 -0
- package/ts/email/helpers.inbound-security.ts +45 -0
- package/ts/email/index.ts +2 -0
- package/ts/opsserver/handlers/email-ops.handler.ts +214 -15
- package/ts/remoteingress/classes.tunnel-manager.ts +14 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate/email-ops.ts +129 -3
- package/ts_web/appstate/runtime.ts +13 -2
- package/ts_web/elements/email/ops-view-email-security.ts +56 -16
- package/ts_web/elements/email/ops-view-emails.ts +130 -2
- 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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
66
|
+
"@serve.zone/catalog": "^2.21.0",
|
|
67
67
|
"@serve.zone/interfaces": "^19.2.0",
|
|
68
|
-
"@serve.zone/remoteingress": "^5.0
|
|
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.
|
package/ts/00_commitinfo_data.ts
CHANGED
package/ts/classes.dcrouter.ts
CHANGED
|
@@ -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:
|
|
475
|
+
filter: ICachedEmailListFilter = {},
|
|
375
476
|
): Promise<CachedEmail[]> {
|
|
376
|
-
const query
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
381
|
-
|
|
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
|
-
|
|
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
|
/**
|