@serve.zone/dcrouter 18.2.0 → 18.3.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 (42) hide show
  1. package/deno.json +1 -1
  2. package/dist_serve/bundle.js +2254 -2315
  3. package/dist_ts/00_commitinfo_data.js +1 -1
  4. package/dist_ts/classes.dcrouter.d.ts +5 -3
  5. package/dist_ts/classes.dcrouter.js +1 -1
  6. package/dist_ts/db/documents/classes.cached.email.d.ts +17 -4
  7. package/dist_ts/db/documents/classes.cached.email.js +105 -8
  8. package/dist_ts/email/classes.accepted-email-spool.d.ts +1 -8
  9. package/dist_ts/email/classes.accepted-email-spool.js +3 -40
  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 +36 -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 +3 -0
  17. package/dist_ts/opsserver/handlers/email-ops.handler.js +84 -6
  18. package/dist_ts_interfaces/requests/email-ops.d.ts +41 -0
  19. package/dist_ts_migrations/index.js +47 -14
  20. package/dist_ts_web/00_commitinfo_data.js +1 -1
  21. package/dist_ts_web/appstate/email-ops.d.ts +12 -1
  22. package/dist_ts_web/appstate/email-ops.js +77 -4
  23. package/dist_ts_web/appstate/runtime.js +11 -3
  24. package/dist_ts_web/elements/email/ops-view-email-security.d.ts +2 -0
  25. package/dist_ts_web/elements/email/ops-view-email-security.js +58 -16
  26. package/dist_ts_web/elements/email/ops-view-emails.d.ts +7 -2
  27. package/dist_ts_web/elements/email/ops-view-emails.js +78 -4
  28. package/package.json +3 -3
  29. package/ts/00_commitinfo_data.ts +1 -1
  30. package/ts/classes.dcrouter.ts +6 -3
  31. package/ts/db/documents/classes.cached.email.ts +141 -7
  32. package/ts/email/classes.accepted-email-spool.ts +5 -52
  33. package/ts/email/helpers.email-log-traffic.ts +117 -0
  34. package/ts/email/helpers.inbound-security.ts +46 -0
  35. package/ts/email/index.ts +2 -0
  36. package/ts/opsserver/handlers/email-ops.handler.ts +116 -4
  37. package/ts_web/00_commitinfo_data.ts +1 -1
  38. package/ts_web/appstate/email-ops.ts +99 -3
  39. package/ts_web/appstate/runtime.ts +13 -2
  40. package/ts_web/elements/email/ops-view-email-security.ts +56 -16
  41. package/ts_web/elements/email/ops-view-emails.ts +74 -2
  42. 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,12 @@ 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 = [];
57
63
  var OpsViewEmails = class extends _classSuper {
58
64
  static { _classThis = this; }
59
65
  static {
@@ -62,10 +68,14 @@ let OpsViewEmails = (() => {
62
68
  _selectedEmail_decorators = [state()];
63
69
  _currentView_decorators = [state()];
64
70
  _isLoading_decorators = [state()];
71
+ _traffic_decorators = [state()];
72
+ _searchQuery_decorators = [state()];
65
73
  __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
74
  __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
75
  __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
76
  __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);
77
+ __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);
78
+ __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);
69
79
  __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
70
80
  OpsViewEmails = _classThis = _classDescriptor.value;
71
81
  if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
@@ -82,13 +92,22 @@ let OpsViewEmails = (() => {
82
92
  #isLoading_accessor_storage = (__runInitializers(this, _currentView_extraInitializers), __runInitializers(this, _isLoading_initializers, false));
83
93
  get isLoading() { return this.#isLoading_accessor_storage; }
84
94
  set isLoading(value) { this.#isLoading_accessor_storage = value; }
85
- stateSubscription = __runInitializers(this, _isLoading_extraInitializers);
95
+ #traffic_accessor_storage = (__runInitializers(this, _isLoading_extraInitializers), __runInitializers(this, _traffic_initializers, null));
96
+ get traffic() { return this.#traffic_accessor_storage; }
97
+ set traffic(value) { this.#traffic_accessor_storage = value; }
98
+ #searchQuery_accessor_storage = (__runInitializers(this, _traffic_extraInitializers), __runInitializers(this, _searchQuery_initializers, ''));
99
+ get searchQuery() { return this.#searchQuery_accessor_storage; }
100
+ set searchQuery(value) { this.#searchQuery_accessor_storage = value; }
101
+ stateSubscription = __runInitializers(this, _searchQuery_extraInitializers);
86
102
  lastEmailOpsUpdate = 0;
87
103
  detailRequestGeneration = 0;
104
+ searchDebounceTimer;
88
105
  async connectedCallback() {
89
106
  await super.connectedCallback();
90
107
  this.stateSubscription = appstate.emailOpsStatePart.select().subscribe((state) => {
91
108
  this.emails = state.emails;
109
+ this.traffic = state.traffic;
110
+ this.searchQuery = state.searchQuery;
92
111
  const hasNewSnapshot = state.lastUpdated > this.lastEmailOpsUpdate;
93
112
  this.lastEmailOpsUpdate = Math.max(this.lastEmailOpsUpdate, state.lastUpdated);
94
113
  if (hasNewSnapshot && this.selectedEmail) {
@@ -104,6 +123,10 @@ let OpsViewEmails = (() => {
104
123
  if (this.stateSubscription) {
105
124
  this.stateSubscription.unsubscribe();
106
125
  }
126
+ if (this.searchDebounceTimer !== undefined) {
127
+ window.clearTimeout(this.searchDebounceTimer);
128
+ this.searchDebounceTimer = undefined;
129
+ }
107
130
  }
108
131
  static styles = [
109
132
  cssManager.defaultStyles,
@@ -117,6 +140,10 @@ let OpsViewEmails = (() => {
117
140
  .viewContainer {
118
141
  height: 100%;
119
142
  }
143
+
144
+ dees-chart-area[slot='above-table'] {
145
+ min-height: 360px;
146
+ }
120
147
  `,
121
148
  ];
122
149
  render() {
@@ -133,8 +160,24 @@ let OpsViewEmails = (() => {
133
160
  : html `
134
161
  <sz-mta-list-view
135
162
  .emails=${this.emails}
163
+ .searchQuery=${this.searchQuery}
164
+ .stickyFilterBar=${true}
165
+ @search-change=${this.handleSearchChange}
136
166
  @email-click=${this.handleEmailClick}
137
- ></sz-mta-list-view>
167
+ >
168
+ ${this.traffic ? html `
169
+ <dees-chart-area
170
+ slot="above-table"
171
+ .label=${this.traffic.label}
172
+ .series=${this.getEmailTrafficSeries()}
173
+ .realtimeMode=${true}
174
+ .rollingWindow=${this.traffic.durationMs}
175
+ .legendStats=${['latest']}
176
+ .chartLines=${[]}
177
+ .yAxisFormatter=${(valueArg) => `${valueArg}`}
178
+ ></dees-chart-area>
179
+ ` : html ``}
180
+ </sz-mta-list-view>
138
181
  `}
139
182
  </div>
140
183
  `;
@@ -162,6 +205,37 @@ let OpsViewEmails = (() => {
162
205
  async handleEmailClick(e) {
163
206
  await this.refreshEmailDetail(e.detail.id, true);
164
207
  }
208
+ handleSearchChange(eventArg) {
209
+ const searchQuery = appstate.setActiveEmailSearchQuery(eventArg.detail.searchQuery);
210
+ if (this.searchDebounceTimer !== undefined) {
211
+ window.clearTimeout(this.searchDebounceTimer);
212
+ }
213
+ this.searchDebounceTimer = window.setTimeout(() => {
214
+ this.searchDebounceTimer = undefined;
215
+ void appstate.emailOpsStatePart.dispatchAction(appstate.fetchAllEmailsAction, { searchQuery });
216
+ }, 300);
217
+ }
218
+ getEmailTrafficSeries() {
219
+ if (!this.traffic)
220
+ return [];
221
+ return [
222
+ {
223
+ name: 'Sent',
224
+ color: '#22c55e',
225
+ data: this.traffic.sent.map((point) => ({ x: point.timestamp, y: point.value })),
226
+ },
227
+ {
228
+ name: 'Received',
229
+ color: '#3b82f6',
230
+ data: this.traffic.received.map((point) => ({ x: point.timestamp, y: point.value })),
231
+ },
232
+ {
233
+ name: 'Failed',
234
+ color: '#ef4444',
235
+ data: this.traffic.failed.map((point) => ({ x: point.timestamp, y: point.value })),
236
+ },
237
+ ];
238
+ }
165
239
  handleBack() {
166
240
  this.detailRequestGeneration++;
167
241
  this.selectedEmail = null;
@@ -174,4 +248,4 @@ let OpsViewEmails = (() => {
174
248
  return OpsViewEmails = _classThis;
175
249
  })();
176
250
  export { OpsViewEmails };
177
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3BzLXZpZXctZW1haWxzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vdHNfd2ViL2VsZW1lbnRzL2VtYWlsL29wcy12aWV3LWVtYWlscy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0FBQUEsT0FBTyxFQUFFLFdBQVcsRUFBRSxRQUFRLEVBQUUsSUFBSSxFQUFFLGFBQWEsRUFBdUIsR0FBRyxFQUFFLEtBQUssRUFBRSxVQUFVLEVBQUUsTUFBTSw2QkFBNkIsQ0FBQztBQUN0SSxPQUFPLEtBQUssUUFBUSxNQUFNLG1CQUFtQixDQUFDO0FBQzlDLE9BQU8sS0FBSyxNQUFNLE1BQU0sb0JBQW9CLENBQUM7QUFDN0MsT0FBTyxLQUFLLFVBQVUsTUFBTSxzQ0FBc0MsQ0FBQztJQVN0RCxhQUFhOzRCQUR6QixhQUFhLENBQUMsaUJBQWlCLENBQUM7Ozs7c0JBQ0UsV0FBVzs7Ozs7Ozs7Ozs7Ozs2QkFBbkIsU0FBUSxXQUFXOzs7O2tDQUMzQyxLQUFLLEVBQUU7eUNBR1AsS0FBSyxFQUFFO3VDQUdQLEtBQUssRUFBRTtxQ0FHUCxLQUFLLEVBQUU7WUFSUix1S0FBUyxNQUFNLDZCQUFOLE1BQU0sdUZBQW9DO1lBR25ELDRMQUFTLGFBQWEsNkJBQWIsYUFBYSxxR0FBaUQ7WUFHdkUsc0xBQVMsV0FBVyw2QkFBWCxXQUFXLGlHQUE2QjtZQUdqRCxnTEFBUyxTQUFTLDZCQUFULFNBQVMsNkZBQVM7WUFYN0IsNktBdUdDOzs7O1FBckdDLHlFQUFnRCxFQUFFLEVBQUM7UUFBbkQsSUFBUyxNQUFNLDRDQUFvQztRQUFuRCxJQUFTLE1BQU0sa0RBQW9DO1FBR25ELDRJQUFrRSxJQUFJLEdBQUM7UUFBdkUsSUFBUyxhQUFhLG1EQUFpRDtRQUF2RSxJQUFTLGFBQWEseURBQWlEO1FBR3ZFLCtJQUEwQyxNQUFNLEdBQUM7UUFBakQsSUFBUyxXQUFXLGlEQUE2QjtRQUFqRCxJQUFTLFdB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
251
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3BzLXZpZXctZW1haWxzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vdHNfd2ViL2VsZW1lbnRzL2VtYWlsL29wcy12aWV3LWVtYWlscy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0FBQUEsT0FBTyxFQUFFLFdBQVcsRUFBRSxJQUFJLEVBQUUsYUFBYSxFQUFFLEdBQUcsRUFBRSxLQUFLLEVBQUUsVUFBVSxFQUFFLE1BQU0sNkJBQTZCLENBQUM7QUFDdkcsT0FBTyxLQUFLLFFBQVEsTUFBTSxtQkFBbUIsQ0FBQztBQUM5QyxPQUFPLEtBQUssTUFBTSxNQUFNLG9CQUFvQixDQUFDO0FBQzdDLE9BQU8sS0FBSyxVQUFVLE1BQU0sc0NBQXNDLENBQUM7SUFTdEQsYUFBYTs0QkFEekIsYUFBYSxDQUFDLGlCQUFpQixDQUFDOzs7O3NCQUNFLFdBQVc7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7NkJBQW5CLFNBQVEsV0FBVzs7OztrQ0FDM0MsS0FBSyxFQUFFO3lDQUdQLEtBQUssRUFBRTt1Q0FHUCxLQUFLLEVBQUU7cUNBR1AsS0FBSyxFQUFFO21DQUdQLEtBQUssRUFBRTt1Q0FHUCxLQUFLLEVBQUU7WUFkUix1S0FBUyxNQUFNLDZCQUFOLE1BQU0sdUZBQW9DO1lBR25ELDRMQUFTLGFBQWEsNkJBQWIsYUFBYSxxR0FBaUQ7WUFHdkUsc0xBQVMsV0FBVyw2QkFBWCxXQUFXLGlHQUE2QjtZQUdqRCxnTEFBUyxTQUFTLDZCQUFULFNBQVMsNkZBQVM7WUFHM0IsMEtBQVMsT0FBTyw2QkFBUCxPQUFPLHlGQUFxRDtZQUdyRSxzTEFBUyxXQUFXLDZCQUFYLFdBQVcsaUdBQU07WUFqQjVCLDZLQStLQzs7OztRQTdLQyx5RUFBZ0QsRUFBRSxFQUFDO1FBQW5ELElBQVMsTUFBTSw0Q0FBb0M7UUFBbkQsSUFBUyxNQUFNLGtEQUFvQztRQUduRCw0SUFBa0UsSUFBSSxHQUFDO1FBQXZFLElB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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.0",
4
+ "version": "18.3.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"
@@ -63,7 +63,7 @@
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.20.0",
67
67
  "@serve.zone/interfaces": "^19.2.0",
68
68
  "@serve.zone/remoteingress": "^5.0.3",
69
69
  "@tsclass/tsclass": "^9.5.1",
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '18.2.0',
6
+ version: '18.3.0',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -65,9 +65,12 @@ export interface IDcRouterEmailTlsConfig extends NonNullable<IUnifiedEmailServer
65
65
  }
66
66
 
67
67
  /** dcrouter's email server options: SmartMTA's, with the path-shaped tls block. */
68
- export type IDcRouterEmailConfig = Omit<IUnifiedEmailServerOptions, 'tls'> & {
68
+ export interface IDcRouterEmailConfig extends Omit<IUnifiedEmailServerOptions, 'tls'> {
69
69
  tls?: IDcRouterEmailTlsConfig;
70
- };
70
+ }
71
+
72
+ /** Convention-correct alias for dcrouter's email server options. */
73
+ export type TDcRouterEmailConfig = IDcRouterEmailConfig;
71
74
 
72
75
  /** Whether an email configuration carries any SMTP AUTH credentials. */
73
76
  const emailConfigHasAuth = (emailConfigArg: IUnifiedEmailServerOptions | undefined): boolean =>
@@ -92,7 +95,7 @@ export interface IDcRouterOptions {
92
95
  * Email server configuration
93
96
  * This enables all email handling with pattern-based routing
94
97
  */
95
- emailConfig?: IDcRouterEmailConfig;
98
+ emailConfig?: TDcRouterEmailConfig;
96
99
 
97
100
  /** SmartBucket configuration for durable SmartMTA queue and attachment blobs. */
98
101
  emailBlobStorage?: ISmartMtaBlobStorageConfig;
@@ -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
@@ -14,6 +15,19 @@ export type TCachedEmailStatus = 'pending' | 'processing' | 'queued' | 'delivere
14
15
  */
15
16
  export type TCachedEmailDirection = 'inbound' | 'outbound';
16
17
 
18
+ export interface ICachedEmailListFilter {
19
+ direction?: TCachedEmailDirection;
20
+ recipientDomain?: string;
21
+ search?: string;
22
+ }
23
+
24
+ export interface ICachedEmailTrafficBucket {
25
+ bucketStart: number;
26
+ sent: number;
27
+ received: number;
28
+ failed: number;
29
+ }
30
+
17
31
  export type TCachedEmailSmtpDeliveryPhase =
18
32
  | 'connect'
19
33
  | 'greeting'
@@ -222,6 +236,7 @@ export class CachedEmail extends plugins.smartdata.SmartdataCachedDocument<Cache
222
236
  /**
223
237
  * Epoch ms when the message was accepted (for newest-first listings)
224
238
  */
239
+ @plugins.smartdata.index()
225
240
  @plugins.smartdata.svDb()
226
241
  public acceptedAt: number = 0;
227
242
 
@@ -366,21 +381,140 @@ export class CachedEmail extends plugins.smartdata.SmartdataCachedDocument<Cache
366
381
  }
367
382
  }
368
383
 
384
+ private static getNativeCollection() {
385
+ return getDb().mongoDb.collection('CachedEmail');
386
+ }
387
+
388
+ private static buildListQuery(filterArg: ICachedEmailListFilter): Record<string, any> {
389
+ const query: Record<string, any> = {};
390
+ if (filterArg.direction) {
391
+ query.direction = filterArg.direction;
392
+ }
393
+ if (filterArg.recipientDomain) {
394
+ query.recipientDomains = filterArg.recipientDomain.toLowerCase();
395
+ }
396
+ if (filterArg.search) {
397
+ if (typeof filterArg.search !== 'string' || filterArg.search.length > 200) {
398
+ throw new Error('Invalid CachedEmail search');
399
+ }
400
+ const escapedSearch = filterArg.search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
401
+ query.$or = [
402
+ { from: { $regex: escapedSearch, $options: 'i' } },
403
+ { to: { $regex: escapedSearch, $options: 'i' } },
404
+ { subject: { $regex: escapedSearch, $options: 'i' } },
405
+ { messageId: { $regex: escapedSearch, $options: 'i' } },
406
+ ];
407
+ }
408
+ return query;
409
+ }
410
+
369
411
  /**
370
412
  * Find the most recently accepted emails, newest first.
371
413
  */
372
414
  public static async findRecent(
373
415
  limit = 200,
374
- filter: { direction?: TCachedEmailDirection; recipientDomain?: string } = {},
416
+ filter: ICachedEmailListFilter = {},
375
417
  ): Promise<CachedEmail[]> {
376
- const query: Record<string, any> = {};
377
- if (filter.direction) {
378
- query.direction = filter.direction;
418
+ const query = CachedEmail.buildListQuery(filter);
419
+ return await CachedEmail.findLimited(query, limit, { acceptedAt: -1 });
420
+ }
421
+
422
+ public static async findOldestAcceptedAt(
423
+ filterArg: ICachedEmailListFilter,
424
+ cutoffArg: number,
425
+ ): Promise<number | null> {
426
+ if (!Number.isSafeInteger(cutoffArg) || cutoffArg < 0) {
427
+ throw new Error('CachedEmail.findOldestAcceptedAt requires a valid cutoff');
379
428
  }
380
- if (filter.recipientDomain) {
381
- query.recipientDomains = filter.recipientDomain.toLowerCase();
429
+ const query = CachedEmail.buildListQuery(filterArg);
430
+ query.acceptedAt = { $gte: cutoffArg };
431
+ const collection = await CachedEmail.getNativeCollection();
432
+ const row = await collection.findOne(
433
+ query,
434
+ {
435
+ projection: { acceptedAt: 1 },
436
+ sort: { acceptedAt: 1 },
437
+ timeoutMS: DB_OPERATION_TIMEOUT_MS,
438
+ },
439
+ );
440
+ const acceptedAt = Number(row?.acceptedAt);
441
+ return Number.isSafeInteger(acceptedAt) && acceptedAt >= 0 ? acceptedAt : null;
442
+ }
443
+
444
+ public static async aggregateEmailLogTraffic(
445
+ filterArg: ICachedEmailListFilter,
446
+ windowStartArg: number,
447
+ windowEndArg: number,
448
+ bucketSizeMsArg: number,
449
+ ): Promise<ICachedEmailTrafficBucket[]> {
450
+ if (
451
+ !Number.isSafeInteger(windowStartArg)
452
+ || !Number.isSafeInteger(windowEndArg)
453
+ || !Number.isSafeInteger(bucketSizeMsArg)
454
+ || windowStartArg < 0
455
+ || windowEndArg < windowStartArg
456
+ || bucketSizeMsArg <= 0
457
+ ) {
458
+ throw new Error('CachedEmail.aggregateEmailLogTraffic received an invalid window');
382
459
  }
383
- return await CachedEmail.findLimited(query, limit, { acceptedAt: -1 });
460
+ const query = CachedEmail.buildListQuery(filterArg);
461
+ query.acceptedAt = { $gte: windowStartArg, $lte: windowEndArg };
462
+ const collection = await CachedEmail.getNativeCollection();
463
+ const cursor = collection.find(
464
+ query,
465
+ {
466
+ projection: { acceptedAt: 1, direction: 1, status: 1 },
467
+ batchSize: 1000,
468
+ timeoutMS: DB_OPERATION_TIMEOUT_MS,
469
+ },
470
+ );
471
+ try {
472
+ const buckets = new Map<number, ICachedEmailTrafficBucket>();
473
+ while (await cursor.hasNext()) {
474
+ const row = await cursor.next();
475
+ if (!row) {
476
+ continue;
477
+ }
478
+ const acceptedAt = Number(row.acceptedAt);
479
+ if (!Number.isSafeInteger(acceptedAt)) {
480
+ continue;
481
+ }
482
+ const bucketStart = Math.floor(acceptedAt / bucketSizeMsArg) * bucketSizeMsArg;
483
+ let bucket = buckets.get(bucketStart);
484
+ if (!bucket) {
485
+ bucket = { bucketStart, sent: 0, received: 0, failed: 0 };
486
+ buckets.set(bucketStart, bucket);
487
+ }
488
+ if (row.status === 'failed' || row.status === 'rejected') {
489
+ bucket.failed++;
490
+ } else if (row.direction === 'outbound') {
491
+ bucket.sent++;
492
+ } else if (row.direction === 'inbound') {
493
+ bucket.received++;
494
+ }
495
+ }
496
+ return [...buckets.values()].sort((firstArg, secondArg) => (
497
+ firstArg.bucketStart - secondArg.bucketStart
498
+ ));
499
+ } finally {
500
+ await cursor.close({ timeoutMS: DB_OPERATION_TIMEOUT_MS });
501
+ }
502
+ }
503
+
504
+ public static async findRecentSecurityCandidates(
505
+ limitArg: number,
506
+ cutoffArg: number,
507
+ ): Promise<CachedEmail[]> {
508
+ const safeLimit = Math.min(Math.max(Math.floor(limitArg), 1), 1000);
509
+ if (!Number.isSafeInteger(cutoffArg) || cutoffArg < 0) {
510
+ throw new Error('CachedEmail.findRecentSecurityCandidates requires a valid cutoff');
511
+ }
512
+ return await CachedEmail.findLimited({
513
+ direction: 'inbound',
514
+ status: { $in: ['flagged', 'rejected', 'deferred'] },
515
+ inboundSecurityResults: { $exists: true, $ne: '' },
516
+ acceptedAt: { $gte: cutoffArg },
517
+ }, safeLimit, { acceptedAt: -1 });
384
518
  }
385
519
 
386
520
  /**
@@ -18,58 +18,11 @@ import type {
18
18
  UnifiedEmailServer,
19
19
  } from '@push.rocks/smartmta';
20
20
  import type { DcRouter } from '../classes.dcrouter.js';
21
-
22
-
23
- /** Inbound authentication verdict evaluation for acceptance semantics. */
24
- export interface IInboundAcceptanceEvaluation {
25
- status: 'accepted' | 'flagged';
26
- doubts: string[];
27
- dmarcReject: boolean;
28
- dmarcDomain?: string;
29
- }
30
-
31
- export function evaluateInboundAcceptance(securityResults: any): IInboundAcceptanceEvaluation {
32
- const doubts: string[] = [];
33
- let dmarcReject = false;
34
- let dmarcDomain: string | undefined;
35
-
36
- // Infrastructure failures (temperror, resolver problems) are OUR problem,
37
- // not evidence against the sender — they never produce doubts.
38
- const spfResult = String(securityResults?.spf?.result || '').toLowerCase();
39
- if (spfResult === 'fail' || spfResult === 'softfail') {
40
- doubts.push(`SPF ${spfResult} for ${securityResults?.spf?.domain || 'sender domain'}`);
41
- }
42
-
43
- // The verdict bridge emits a placeholder entry (status 'none') when a
44
- // message carries no DKIM signature at all; only real failed signatures
45
- // count as doubts.
46
- const dkimSignatures = (Array.isArray(securityResults?.dkim) ? securityResults.dkim : [])
47
- .filter((signature: any) => String(signature?.status || '').toLowerCase() !== 'none');
48
- if (dkimSignatures.length > 0 && !dkimSignatures.some((signature: any) => signature?.is_valid)) {
49
- doubts.push('DKIM signature(s) present but none verified');
50
- }
51
-
52
- // DMARC only matters when the sender domain publishes an enforcing policy;
53
- // absent records surface as policy 'none' and are not doubts.
54
- const dmarc = securityResults?.dmarc;
55
- if (dmarc && dmarc.passed === false) {
56
- const policy = String(dmarc.policy || '').toLowerCase();
57
- const action = String(dmarc.action || '').toLowerCase();
58
- if (policy === 'reject' || policy === 'quarantine' || action === 'reject' || action === 'quarantine') {
59
- dmarcDomain = dmarc.domain || undefined;
60
- if (action === 'reject' || policy === 'reject') {
61
- dmarcReject = true;
62
- }
63
- doubts.push(`DMARC failed for ${dmarc.domain || 'sender domain'} (policy ${dmarc.policy || 'unknown'})`);
64
- }
65
- }
66
- return {
67
- status: doubts.length > 0 ? 'flagged' : 'accepted',
68
- doubts,
69
- dmarcReject,
70
- dmarcDomain,
71
- };
72
- }
21
+ import { evaluateInboundAcceptance } from './helpers.inbound-security.js';
22
+ export {
23
+ evaluateInboundAcceptance,
24
+ type IInboundAcceptanceEvaluation,
25
+ } from './helpers.inbound-security.js';
73
26
 
74
27
  export const DCROUTER_CACHE_ID_HEADER = 'X-Dcrouter-Cached-Email-Id';
75
28
  const ACCEPTED_EMAIL_SPOOL_INTERVAL_MS = 60_000;
@@ -0,0 +1,117 @@
1
+ import type {
2
+ IEmail,
3
+ IEmailLogTraffic,
4
+ TEmailLogTrafficWindow,
5
+ } from '../../ts_interfaces/requests/email-ops.js';
6
+
7
+ const MINUTE_MS = 60_000;
8
+ const HOUR_MS = 60 * MINUTE_MS;
9
+ const DAY_MS = 24 * HOUR_MS;
10
+ export const EMAIL_LOG_RETENTION_MS = 30 * DAY_MS;
11
+ export const EMAIL_LOG_SEARCH_MAX_LENGTH = 200;
12
+
13
+ export interface IEmailLogTrafficWindowDescriptor {
14
+ window: TEmailLogTrafficWindow;
15
+ label: string;
16
+ durationMs: number;
17
+ bucketSizeMs: number;
18
+ }
19
+
20
+ export interface IEmailLogSparseTrafficBucket {
21
+ bucketStart: number;
22
+ sent: number;
23
+ received: number;
24
+ failed: number;
25
+ }
26
+
27
+ const trafficWindows: IEmailLogTrafficWindowDescriptor[] = [
28
+ { window: '24h', label: 'Email Traffic — Last 24 Hours', durationMs: DAY_MS, bucketSizeMs: MINUTE_MS },
29
+ { window: '7d', label: 'Email Traffic — Last 7 Days', durationMs: 7 * DAY_MS, bucketSizeMs: 15 * MINUTE_MS },
30
+ { window: '14d', label: 'Email Traffic — Last 14 Days', durationMs: 14 * DAY_MS, bucketSizeMs: 30 * MINUTE_MS },
31
+ { window: '30d', label: 'Email Traffic — Last 30 Days', durationMs: 30 * DAY_MS, bucketSizeMs: HOUR_MS },
32
+ ];
33
+
34
+ export function normalizeEmailLogSearch(searchArg: unknown): string {
35
+ if (searchArg === undefined || searchArg === null) return '';
36
+ if (typeof searchArg !== 'string') {
37
+ throw new Error('Email log search must be a string');
38
+ }
39
+ const search = searchArg.trim();
40
+ if (search.length > EMAIL_LOG_SEARCH_MAX_LENGTH) {
41
+ throw new Error(`Email log search accepts at most ${EMAIL_LOG_SEARCH_MAX_LENGTH} characters`);
42
+ }
43
+ return search;
44
+ }
45
+
46
+ export function selectEmailLogTrafficWindow(
47
+ searchArg: string,
48
+ oldestMatchAtArg: number | null,
49
+ nowArg = Date.now(),
50
+ ): IEmailLogTrafficWindowDescriptor {
51
+ if (!searchArg || oldestMatchAtArg === null || !Number.isFinite(oldestMatchAtArg)) {
52
+ return trafficWindows[0];
53
+ }
54
+ return trafficWindows.find((window) => {
55
+ const lastBucketStart = Math.floor(nowArg / window.bucketSizeMs) * window.bucketSizeMs;
56
+ const windowStart = lastBucketStart - window.durationMs + window.bucketSizeMs;
57
+ return oldestMatchAtArg >= windowStart;
58
+ }) || trafficWindows.at(-1)!;
59
+ }
60
+
61
+ export function buildEmailLogTraffic(
62
+ descriptorArg: IEmailLogTrafficWindowDescriptor,
63
+ sparseBucketsArg: IEmailLogSparseTrafficBucket[],
64
+ nowArg = Date.now(),
65
+ ): IEmailLogTraffic {
66
+ const lastBucketStart = Math.floor(nowArg / descriptorArg.bucketSizeMs) * descriptorArg.bucketSizeMs;
67
+ const windowStart = lastBucketStart - descriptorArg.durationMs + descriptorArg.bucketSizeMs;
68
+ const bucketsByStart = new Map(
69
+ sparseBucketsArg.map((bucket) => [bucket.bucketStart, bucket]),
70
+ );
71
+ const sent: IEmailLogTraffic['sent'] = [];
72
+ const received: IEmailLogTraffic['received'] = [];
73
+ const failed: IEmailLogTraffic['failed'] = [];
74
+ for (
75
+ let timestamp = windowStart;
76
+ timestamp <= lastBucketStart;
77
+ timestamp += descriptorArg.bucketSizeMs
78
+ ) {
79
+ const bucket = bucketsByStart.get(timestamp);
80
+ sent.push({ timestamp, value: Math.max(0, Number(bucket?.sent || 0)) });
81
+ received.push({ timestamp, value: Math.max(0, Number(bucket?.received || 0)) });
82
+ failed.push({ timestamp, value: Math.max(0, Number(bucket?.failed || 0)) });
83
+ }
84
+ return {
85
+ ...descriptorArg,
86
+ windowStart,
87
+ windowEnd: nowArg,
88
+ sent,
89
+ received,
90
+ failed,
91
+ };
92
+ }
93
+
94
+ export function aggregateEmailLogTrafficInMemory(
95
+ emailsArg: IEmail[],
96
+ descriptorArg: IEmailLogTrafficWindowDescriptor,
97
+ nowArg = Date.now(),
98
+ ): IEmailLogSparseTrafficBucket[] {
99
+ const lastBucketStart = Math.floor(nowArg / descriptorArg.bucketSizeMs) * descriptorArg.bucketSizeMs;
100
+ const windowStart = lastBucketStart - descriptorArg.durationMs + descriptorArg.bucketSizeMs;
101
+ const buckets = new Map<number, IEmailLogSparseTrafficBucket>();
102
+ for (const email of emailsArg) {
103
+ const timestamp = new Date(email.timestamp).getTime();
104
+ if (!Number.isFinite(timestamp) || timestamp < windowStart || timestamp > nowArg) continue;
105
+ const bucketStart = Math.floor(timestamp / descriptorArg.bucketSizeMs) * descriptorArg.bucketSizeMs;
106
+ const bucket = buckets.get(bucketStart) || { bucketStart, sent: 0, received: 0, failed: 0 };
107
+ if (email.status === 'bounced' || email.status === 'rejected') {
108
+ bucket.failed++;
109
+ } else if (email.direction === 'outbound') {
110
+ bucket.sent++;
111
+ } else {
112
+ bucket.received++;
113
+ }
114
+ buckets.set(bucketStart, bucket);
115
+ }
116
+ return [...buckets.values()].sort((a, b) => a.bucketStart - b.bucketStart);
117
+ }
@@ -0,0 +1,46 @@
1
+ /** Inbound authentication verdict evaluation shared by SMTP acceptance and ops reporting. */
2
+ export interface IInboundAcceptanceEvaluation {
3
+ status: 'accepted' | 'flagged';
4
+ doubts: string[];
5
+ dmarcReject: boolean;
6
+ dmarcDomain?: string;
7
+ }
8
+
9
+ export function evaluateInboundAcceptance(securityResults: any): IInboundAcceptanceEvaluation {
10
+ const doubts: string[] = [];
11
+ let dmarcReject = false;
12
+ let dmarcDomain: string | undefined;
13
+
14
+ // Infrastructure failures are our problem, not evidence against the sender.
15
+ const spfResult = String(securityResults?.spf?.result || '').toLowerCase();
16
+ if (spfResult === 'fail' || spfResult === 'softfail') {
17
+ doubts.push(`SPF ${spfResult} for ${securityResults?.spf?.domain || 'sender domain'}`);
18
+ }
19
+
20
+ // Ignore the placeholder emitted when no DKIM signature exists.
21
+ const dkimSignatures = (Array.isArray(securityResults?.dkim) ? securityResults.dkim : [])
22
+ .filter((signature: any) => String(signature?.status || '').toLowerCase() !== 'none');
23
+ if (dkimSignatures.length > 0 && !dkimSignatures.some((signature: any) => signature?.is_valid)) {
24
+ doubts.push('DKIM signature(s) present but none verified');
25
+ }
26
+
27
+ // Only enforcing DMARC policies produce a security finding.
28
+ const dmarc = securityResults?.dmarc;
29
+ if (dmarc && dmarc.passed === false) {
30
+ const policy = String(dmarc.policy || '').toLowerCase();
31
+ const action = String(dmarc.action || '').toLowerCase();
32
+ if (policy === 'reject' || policy === 'quarantine' || action === 'reject' || action === 'quarantine') {
33
+ dmarcDomain = dmarc.domain || undefined;
34
+ if (action === 'reject' || policy === 'reject') {
35
+ dmarcReject = true;
36
+ }
37
+ doubts.push(`DMARC failed for ${dmarc.domain || 'sender domain'} (policy ${dmarc.policy || 'unknown'})`);
38
+ }
39
+ }
40
+ return {
41
+ status: doubts.length > 0 ? 'flagged' : 'accepted',
42
+ doubts,
43
+ dmarcReject,
44
+ dmarcDomain,
45
+ };
46
+ }
package/ts/email/index.ts CHANGED
@@ -15,3 +15,5 @@ export * from './classes.smtp-account.manager.js';
15
15
  export * from './smtp-scram.js';
16
16
  export * from './classes.workapp-mail-manager.js';
17
17
  export * from './email-dns-records.js';
18
+ export * from './helpers.email-log-traffic.js';
19
+ export * from './helpers.inbound-security.js';