@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
|
@@ -6,18 +6,32 @@ import { captureRealtimeTicket, isRealtimeTicketCurrent } from './realtime.js';
|
|
|
6
6
|
|
|
7
7
|
export interface IEmailOpsState {
|
|
8
8
|
emails: interfaces.requests.IEmail[];
|
|
9
|
+
traffic: interfaces.requests.IEmailLogTraffic | null;
|
|
10
|
+
searchQuery: string;
|
|
11
|
+
selectedRange: { from: number; to: number } | null;
|
|
12
|
+
securityFindings: interfaces.requests.IEmailSecurityFinding[];
|
|
13
|
+
securityFindingsLoading: boolean;
|
|
14
|
+
securityFindingsError: string | null;
|
|
9
15
|
isLoading: boolean;
|
|
10
16
|
error: string | null;
|
|
11
17
|
lastUpdated: number;
|
|
18
|
+
lastSecurityUpdated: number;
|
|
12
19
|
}
|
|
13
20
|
|
|
14
21
|
export const emailOpsStatePart = await appState.getStatePart<IEmailOpsState>(
|
|
15
22
|
'emailOps',
|
|
16
23
|
{
|
|
17
24
|
emails: [],
|
|
25
|
+
traffic: null,
|
|
26
|
+
searchQuery: '',
|
|
27
|
+
selectedRange: null,
|
|
28
|
+
securityFindings: [],
|
|
29
|
+
securityFindingsLoading: false,
|
|
30
|
+
securityFindingsError: null,
|
|
18
31
|
isLoading: false,
|
|
19
32
|
error: null,
|
|
20
33
|
lastUpdated: 0,
|
|
34
|
+
lastSecurityUpdated: 0,
|
|
21
35
|
},
|
|
22
36
|
'soft'
|
|
23
37
|
);
|
|
@@ -25,11 +39,65 @@ export const emailOpsStatePart = await appState.getStatePart<IEmailOpsState>(
|
|
|
25
39
|
// Email Operations Actions
|
|
26
40
|
// ============================================================================
|
|
27
41
|
|
|
42
|
+
let activeSearchQuery = '';
|
|
43
|
+
let activeSelectedRange: { from: number; to: number } | null = null;
|
|
44
|
+
let emailListRequestGeneration = 0;
|
|
45
|
+
let securityFindingsRequestGeneration = 0;
|
|
46
|
+
|
|
47
|
+
export interface IEmailOpsFetchOptions {
|
|
48
|
+
searchQuery?: string;
|
|
49
|
+
selectedRange?: { from: number; to: number } | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function setActiveEmailSearchQuery(searchQueryArg: string): string {
|
|
53
|
+
const nextSearchQuery = searchQueryArg.trim().slice(0, 200);
|
|
54
|
+
if (nextSearchQuery === activeSearchQuery) return activeSearchQuery;
|
|
55
|
+
activeSearchQuery = nextSearchQuery;
|
|
56
|
+
emailListRequestGeneration++;
|
|
57
|
+
const currentState = emailOpsStatePart.getState()!;
|
|
58
|
+
emailOpsStatePart.setState({
|
|
59
|
+
...currentState,
|
|
60
|
+
searchQuery: activeSearchQuery,
|
|
61
|
+
});
|
|
62
|
+
return activeSearchQuery;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function setActiveEmailSelectedRange(
|
|
66
|
+
rangeArg: { from: number; to: number } | null,
|
|
67
|
+
): { from: number; to: number } | null {
|
|
68
|
+
if (rangeArg && (
|
|
69
|
+
!Number.isSafeInteger(rangeArg.from)
|
|
70
|
+
|| !Number.isSafeInteger(rangeArg.to)
|
|
71
|
+
|| rangeArg.from < 0
|
|
72
|
+
|| rangeArg.to <= rangeArg.from
|
|
73
|
+
)) {
|
|
74
|
+
throw new Error('Invalid email time range');
|
|
75
|
+
}
|
|
76
|
+
activeSelectedRange = rangeArg ? { ...rangeArg } : null;
|
|
77
|
+
emailListRequestGeneration++;
|
|
78
|
+
const currentState = emailOpsStatePart.getState()!;
|
|
79
|
+
emailOpsStatePart.setState({
|
|
80
|
+
...currentState,
|
|
81
|
+
selectedRange: activeSelectedRange,
|
|
82
|
+
});
|
|
83
|
+
return activeSelectedRange;
|
|
84
|
+
}
|
|
85
|
+
|
|
28
86
|
// Fetch All Emails Action
|
|
29
|
-
export const fetchAllEmailsAction = emailOpsStatePart.createAction(async (
|
|
87
|
+
export const fetchAllEmailsAction = emailOpsStatePart.createAction<IEmailOpsFetchOptions | null>(async (
|
|
88
|
+
statePartArg,
|
|
89
|
+
dataArg,
|
|
90
|
+
): Promise<IEmailOpsState> => {
|
|
30
91
|
const context = getActionContext();
|
|
31
92
|
const currentState = statePartArg.getState()!;
|
|
32
93
|
if (!context.identity) return currentState;
|
|
94
|
+
const searchQuery = dataArg?.searchQuery !== undefined
|
|
95
|
+
? setActiveEmailSearchQuery(dataArg.searchQuery)
|
|
96
|
+
: activeSearchQuery;
|
|
97
|
+
const selectedRange = dataArg?.selectedRange !== undefined
|
|
98
|
+
? setActiveEmailSelectedRange(dataArg.selectedRange)
|
|
99
|
+
: activeSelectedRange;
|
|
100
|
+
const requestGeneration = ++emailListRequestGeneration;
|
|
33
101
|
const ticket = captureRealtimeTicket(['emailQueue', 'smtpAttempts']);
|
|
34
102
|
|
|
35
103
|
try {
|
|
@@ -39,18 +107,29 @@ export const fetchAllEmailsAction = emailOpsStatePart.createAction(async (stateP
|
|
|
39
107
|
|
|
40
108
|
const response = await request.fire({
|
|
41
109
|
identity: context.identity,
|
|
110
|
+
...(searchQuery ? { search: searchQuery } : {}),
|
|
111
|
+
...(selectedRange ? selectedRange : {}),
|
|
42
112
|
});
|
|
43
|
-
if (
|
|
113
|
+
if (
|
|
114
|
+
requestGeneration !== emailListRequestGeneration
|
|
115
|
+
|| !isRealtimeTicketCurrent(ticket)
|
|
116
|
+
) return statePartArg.getState()!;
|
|
44
117
|
|
|
45
118
|
return {
|
|
46
119
|
...statePartArg.getState()!,
|
|
47
120
|
emails: response.emails,
|
|
121
|
+
traffic: response.traffic,
|
|
122
|
+
searchQuery,
|
|
48
123
|
isLoading: false,
|
|
124
|
+
selectedRange,
|
|
49
125
|
error: null,
|
|
50
126
|
lastUpdated: Date.now(),
|
|
51
127
|
};
|
|
52
128
|
} catch (error) {
|
|
53
|
-
if (
|
|
129
|
+
if (
|
|
130
|
+
requestGeneration !== emailListRequestGeneration
|
|
131
|
+
|| !isRealtimeTicketCurrent(ticket)
|
|
132
|
+
) return statePartArg.getState()!;
|
|
54
133
|
return {
|
|
55
134
|
...statePartArg.getState()!,
|
|
56
135
|
isLoading: false,
|
|
@@ -59,6 +138,53 @@ export const fetchAllEmailsAction = emailOpsStatePart.createAction(async (stateP
|
|
|
59
138
|
}
|
|
60
139
|
});
|
|
61
140
|
|
|
141
|
+
export const fetchEmailSecurityFindingsAction = emailOpsStatePart.createAction<void>(async (
|
|
142
|
+
statePartArg,
|
|
143
|
+
): Promise<IEmailOpsState> => {
|
|
144
|
+
const context = getActionContext();
|
|
145
|
+
const currentState = statePartArg.getState()!;
|
|
146
|
+
if (!context.identity) return currentState;
|
|
147
|
+
const requestGeneration = ++securityFindingsRequestGeneration;
|
|
148
|
+
const ticket = captureRealtimeTicket(['emailQueue', 'smtpAttempts']);
|
|
149
|
+
statePartArg.setState({
|
|
150
|
+
...currentState,
|
|
151
|
+
securityFindingsLoading: true,
|
|
152
|
+
securityFindingsError: null,
|
|
153
|
+
});
|
|
154
|
+
try {
|
|
155
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
156
|
+
interfaces.requests.IReq_GetEmailSecurityFindings
|
|
157
|
+
>('/typedrequest', 'getEmailSecurityFindings');
|
|
158
|
+
const response = await request.fire({
|
|
159
|
+
identity: context.identity,
|
|
160
|
+
limit: 200,
|
|
161
|
+
});
|
|
162
|
+
if (
|
|
163
|
+
requestGeneration !== securityFindingsRequestGeneration
|
|
164
|
+
|| !isRealtimeTicketCurrent(ticket)
|
|
165
|
+
) return statePartArg.getState()!;
|
|
166
|
+
return {
|
|
167
|
+
...statePartArg.getState()!,
|
|
168
|
+
securityFindings: response.findings,
|
|
169
|
+
securityFindingsLoading: false,
|
|
170
|
+
securityFindingsError: null,
|
|
171
|
+
lastSecurityUpdated: Date.now(),
|
|
172
|
+
};
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (
|
|
175
|
+
requestGeneration !== securityFindingsRequestGeneration
|
|
176
|
+
|| !isRealtimeTicketCurrent(ticket)
|
|
177
|
+
) return statePartArg.getState()!;
|
|
178
|
+
return {
|
|
179
|
+
...statePartArg.getState()!,
|
|
180
|
+
securityFindingsLoading: false,
|
|
181
|
+
securityFindingsError: error instanceof Error
|
|
182
|
+
? error.message
|
|
183
|
+
: 'Failed to fetch email security findings',
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
62
188
|
// ============================================================================
|
|
63
189
|
export async function fetchEmailDetailSnapshot(
|
|
64
190
|
emailIdArg: string,
|
|
@@ -11,7 +11,11 @@ import { certificateStatePart, fetchCertificateOverviewAction } from './certific
|
|
|
11
11
|
import { remoteIngressStatePart, fetchRemoteIngressAction } from './remoteingress.js';
|
|
12
12
|
import { vpnStatePart, fetchVpnAction } from './vpn.js';
|
|
13
13
|
import { emailDomainsStatePart, fetchEmailDomainsAction } from './email-domains.js';
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
emailOpsStatePart,
|
|
16
|
+
fetchAllEmailsAction,
|
|
17
|
+
fetchEmailSecurityFindingsAction,
|
|
18
|
+
} from './email-ops.js';
|
|
15
19
|
import {
|
|
16
20
|
domainsStatePart,
|
|
17
21
|
fetchDnsRecordsForDomainAction,
|
|
@@ -65,7 +69,14 @@ const refreshRealtimeGroup = async (groupArg: TRealtimeRefreshGroup): Promise<vo
|
|
|
65
69
|
await emailDomainsStatePart.dispatchAction(fetchEmailDomainsAction, null);
|
|
66
70
|
break;
|
|
67
71
|
case 'emailOps':
|
|
68
|
-
|
|
72
|
+
if (uiStatePart.getState()!.activeView !== 'email') {
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
if (uiStatePart.getState()!.activeSubview === 'security') {
|
|
76
|
+
await emailOpsStatePart.dispatchAction(fetchEmailSecurityFindingsAction, undefined);
|
|
77
|
+
} else if (uiStatePart.getState()!.activeSubview === 'log') {
|
|
78
|
+
await emailOpsStatePart.dispatchAction(fetchAllEmailsAction, null);
|
|
79
|
+
}
|
|
69
80
|
break;
|
|
70
81
|
case 'dns': {
|
|
71
82
|
const selectedDomainId = domainsStatePart.getState()!.selectedDomainId;
|
|
@@ -23,14 +23,32 @@ export class OpsViewEmailSecurity extends DeesElement {
|
|
|
23
23
|
@state()
|
|
24
24
|
accessor statsState: appstate.IStatsState = appstate.statsStatePart.getState()!;
|
|
25
25
|
|
|
26
|
+
@state()
|
|
27
|
+
accessor emailOpsState: appstate.IEmailOpsState = appstate.emailOpsStatePart.getState()!;
|
|
28
|
+
|
|
26
29
|
constructor() {
|
|
27
30
|
super();
|
|
28
31
|
const sub = appstate.statsStatePart
|
|
29
|
-
.select(
|
|
32
|
+
.select()
|
|
30
33
|
.subscribe((s) => {
|
|
31
34
|
this.statsState = s;
|
|
32
35
|
});
|
|
33
36
|
this.rxSubscriptions.push(sub);
|
|
37
|
+
|
|
38
|
+
const emailOpsSub = appstate.emailOpsStatePart
|
|
39
|
+
.select()
|
|
40
|
+
.subscribe((s) => {
|
|
41
|
+
this.emailOpsState = s;
|
|
42
|
+
});
|
|
43
|
+
this.rxSubscriptions.push(emailOpsSub);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public async connectedCallback(): Promise<void> {
|
|
47
|
+
await super.connectedCallback();
|
|
48
|
+
await appstate.emailOpsStatePart.dispatchAction(
|
|
49
|
+
appstate.fetchEmailSecurityFindingsAction,
|
|
50
|
+
undefined,
|
|
51
|
+
);
|
|
34
52
|
}
|
|
35
53
|
|
|
36
54
|
public static styles = [
|
|
@@ -42,21 +60,16 @@ export class OpsViewEmailSecurity extends DeesElement {
|
|
|
42
60
|
flex-direction: column;
|
|
43
61
|
gap: 24px;
|
|
44
62
|
}
|
|
63
|
+
|
|
64
|
+
.findingsError {
|
|
65
|
+
color: ${cssManager.bdTheme('#b91c1c', '#fca5a5')};
|
|
66
|
+
}
|
|
45
67
|
`,
|
|
46
68
|
];
|
|
47
69
|
|
|
48
70
|
public render(): TemplateResult {
|
|
49
71
|
const metrics = this.statsState.securityMetrics;
|
|
50
|
-
|
|
51
|
-
if (!metrics) {
|
|
52
|
-
return html`
|
|
53
|
-
<div class="loadingMessage">
|
|
54
|
-
<p>Loading security metrics...</p>
|
|
55
|
-
</div>
|
|
56
|
-
`;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const tiles: IStatsTile[] = [
|
|
72
|
+
const tiles: IStatsTile[] = metrics ? [
|
|
60
73
|
{
|
|
61
74
|
id: 'malware',
|
|
62
75
|
title: 'Malware Detection',
|
|
@@ -93,16 +106,20 @@ export class OpsViewEmailSecurity extends DeesElement {
|
|
|
93
106
|
color: '#f59e0b',
|
|
94
107
|
description: 'Spam emails blocked',
|
|
95
108
|
},
|
|
96
|
-
];
|
|
109
|
+
] : [];
|
|
97
110
|
|
|
98
111
|
return html`
|
|
99
112
|
<dees-heading level="3">Email Security</dees-heading>
|
|
100
113
|
|
|
101
114
|
<div class="securityContainer">
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
115
|
+
${metrics ? html`
|
|
116
|
+
<dees-statsgrid
|
|
117
|
+
.tiles=${tiles}
|
|
118
|
+
.minTileWidth=${200}
|
|
119
|
+
></dees-statsgrid>
|
|
120
|
+
` : html`
|
|
121
|
+
<p class="loadingMessage">Loading security metrics...</p>
|
|
122
|
+
`}
|
|
106
123
|
|
|
107
124
|
<dees-settings
|
|
108
125
|
.heading=${'Security Configuration'}
|
|
@@ -114,6 +131,29 @@ export class OpsViewEmailSecurity extends DeesElement {
|
|
|
114
131
|
]}
|
|
115
132
|
.actions=${[{ name: 'Edit', action: () => this.showEditSecurityDialog() }]}
|
|
116
133
|
></dees-settings>
|
|
134
|
+
|
|
135
|
+
<dees-table
|
|
136
|
+
.heading1=${'Email Security Findings'}
|
|
137
|
+
.heading2=${this.emailOpsState.securityFindingsLoading
|
|
138
|
+
? 'Loading retained findings...'
|
|
139
|
+
: this.emailOpsState.securityFindingsError
|
|
140
|
+
? 'Could not load retained findings'
|
|
141
|
+
: `Last 30 days · ${this.emailOpsState.securityFindings.length} findings`}
|
|
142
|
+
.data=${this.emailOpsState.securityFindings}
|
|
143
|
+
.searchable=${false}
|
|
144
|
+
.rowKey=${'id'}
|
|
145
|
+
.displayFunction=${(finding) => ({
|
|
146
|
+
'Time': new Date(finding.timestamp).toLocaleString(),
|
|
147
|
+
'From': finding.from,
|
|
148
|
+
'To': finding.to,
|
|
149
|
+
'Title': finding.subject || '(no title)',
|
|
150
|
+
'Security issue': `${finding.disposition === 'rejected' ? 'Rejected' : 'Flagged'} — ${finding.failureSummary}`,
|
|
151
|
+
})}
|
|
152
|
+
></dees-table>
|
|
153
|
+
|
|
154
|
+
${this.emailOpsState.securityFindingsError ? html`
|
|
155
|
+
<p class="findingsError">${this.emailOpsState.securityFindingsError}</p>
|
|
156
|
+
` : html``}
|
|
117
157
|
</div>
|
|
118
158
|
`;
|
|
119
159
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DeesElement,
|
|
1
|
+
import { DeesElement, html, customElement, css, state, cssManager } from '@design.estate/dees-element';
|
|
2
2
|
import * as appstate from '../../appstate.js';
|
|
3
3
|
import * as shared from '../shared/index.js';
|
|
4
4
|
import * as interfaces from '../../../dist_ts_interfaces/index.js';
|
|
@@ -23,14 +23,27 @@ export class OpsViewEmails extends DeesElement {
|
|
|
23
23
|
@state()
|
|
24
24
|
accessor isLoading = false;
|
|
25
25
|
|
|
26
|
+
@state()
|
|
27
|
+
accessor traffic: interfaces.requests.IEmailLogTraffic | null = null;
|
|
28
|
+
|
|
29
|
+
@state()
|
|
30
|
+
accessor searchQuery = '';
|
|
31
|
+
|
|
32
|
+
@state()
|
|
33
|
+
accessor selectedRange: { from: number; to: number } | null = null;
|
|
34
|
+
|
|
26
35
|
private stateSubscription: any;
|
|
27
36
|
private lastEmailOpsUpdate = 0;
|
|
28
37
|
private detailRequestGeneration = 0;
|
|
38
|
+
private searchDebounceTimer: number | undefined;
|
|
29
39
|
|
|
30
40
|
async connectedCallback() {
|
|
31
41
|
await super.connectedCallback();
|
|
32
42
|
this.stateSubscription = appstate.emailOpsStatePart.select().subscribe((state) => {
|
|
33
43
|
this.emails = state.emails;
|
|
44
|
+
this.traffic = state.traffic;
|
|
45
|
+
this.searchQuery = state.searchQuery;
|
|
46
|
+
this.selectedRange = state.selectedRange;
|
|
34
47
|
const hasNewSnapshot = state.lastUpdated > this.lastEmailOpsUpdate;
|
|
35
48
|
this.lastEmailOpsUpdate = Math.max(this.lastEmailOpsUpdate, state.lastUpdated);
|
|
36
49
|
if (hasNewSnapshot && this.selectedEmail) {
|
|
@@ -47,6 +60,10 @@ export class OpsViewEmails extends DeesElement {
|
|
|
47
60
|
if (this.stateSubscription) {
|
|
48
61
|
this.stateSubscription.unsubscribe();
|
|
49
62
|
}
|
|
63
|
+
if (this.searchDebounceTimer !== undefined) {
|
|
64
|
+
window.clearTimeout(this.searchDebounceTimer);
|
|
65
|
+
this.searchDebounceTimer = undefined;
|
|
66
|
+
}
|
|
50
67
|
}
|
|
51
68
|
|
|
52
69
|
public static styles = [
|
|
@@ -61,6 +78,26 @@ export class OpsViewEmails extends DeesElement {
|
|
|
61
78
|
.viewContainer {
|
|
62
79
|
height: 100%;
|
|
63
80
|
}
|
|
81
|
+
|
|
82
|
+
.chartStack {
|
|
83
|
+
display: flex;
|
|
84
|
+
flex-direction: column;
|
|
85
|
+
gap: 8px;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.rangeSummary {
|
|
89
|
+
display: flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
justify-content: space-between;
|
|
92
|
+
min-height: 32px;
|
|
93
|
+
padding: 0 4px;
|
|
94
|
+
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
95
|
+
font-size: 13px;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.chartStack dees-chart-area {
|
|
99
|
+
min-height: 360px;
|
|
100
|
+
}
|
|
64
101
|
`,
|
|
65
102
|
];
|
|
66
103
|
|
|
@@ -78,8 +115,34 @@ export class OpsViewEmails extends DeesElement {
|
|
|
78
115
|
: html`
|
|
79
116
|
<sz-mta-list-view
|
|
80
117
|
.emails=${this.emails}
|
|
118
|
+
.searchQuery=${this.searchQuery}
|
|
119
|
+
.stickyFilterBar=${true}
|
|
120
|
+
@search-change=${this.handleSearchChange}
|
|
81
121
|
@email-click=${this.handleEmailClick}
|
|
82
|
-
|
|
122
|
+
>
|
|
123
|
+
${this.traffic ? html`
|
|
124
|
+
<div slot="above-table" class="chartStack">
|
|
125
|
+
${this.selectedRange ? html`
|
|
126
|
+
<div class="rangeSummary">
|
|
127
|
+
<span>${this.formatSelectedRange()}</span>
|
|
128
|
+
<dees-button @click=${this.clearSelectedRange}>Clear time range</dees-button>
|
|
129
|
+
</div>
|
|
130
|
+
` : html``}
|
|
131
|
+
<dees-chart-area
|
|
132
|
+
.label=${this.traffic.label}
|
|
133
|
+
.series=${this.getEmailTrafficSeries()}
|
|
134
|
+
.realtimeMode=${true}
|
|
135
|
+
.rollingWindow=${this.traffic.durationMs}
|
|
136
|
+
.legendStats=${['latest']}
|
|
137
|
+
.chartLines=${[]}
|
|
138
|
+
.rangeSelectionEnabled=${true}
|
|
139
|
+
.selectedRange=${this.selectedRange}
|
|
140
|
+
.yAxisFormatter=${(valueArg: number) => `${valueArg}`}
|
|
141
|
+
@range-change=${this.handleRangeChange}
|
|
142
|
+
></dees-chart-area>
|
|
143
|
+
</div>
|
|
144
|
+
` : html``}
|
|
145
|
+
</sz-mta-list-view>
|
|
83
146
|
`
|
|
84
147
|
}
|
|
85
148
|
</div>
|
|
@@ -108,6 +171,71 @@ export class OpsViewEmails extends DeesElement {
|
|
|
108
171
|
await this.refreshEmailDetail(e.detail.id, true);
|
|
109
172
|
}
|
|
110
173
|
|
|
174
|
+
private handleSearchChange(eventArg: CustomEvent<{ searchQuery: string }>): void {
|
|
175
|
+
const searchQuery = appstate.setActiveEmailSearchQuery(eventArg.detail.searchQuery);
|
|
176
|
+
if (this.searchDebounceTimer !== undefined) {
|
|
177
|
+
window.clearTimeout(this.searchDebounceTimer);
|
|
178
|
+
}
|
|
179
|
+
this.searchDebounceTimer = window.setTimeout(() => {
|
|
180
|
+
this.searchDebounceTimer = undefined;
|
|
181
|
+
void appstate.emailOpsStatePart.dispatchAction(
|
|
182
|
+
appstate.fetchAllEmailsAction,
|
|
183
|
+
{ searchQuery },
|
|
184
|
+
);
|
|
185
|
+
}, 300);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
private handleRangeChange = (
|
|
190
|
+
eventArg: CustomEvent<{ from: number; to: number }>,
|
|
191
|
+
): void => {
|
|
192
|
+
void appstate.emailOpsStatePart.dispatchAction(
|
|
193
|
+
appstate.fetchAllEmailsAction,
|
|
194
|
+
{ selectedRange: eventArg.detail },
|
|
195
|
+
);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
private clearSelectedRange = (): void => {
|
|
199
|
+
void appstate.emailOpsStatePart.dispatchAction(
|
|
200
|
+
appstate.fetchAllEmailsAction,
|
|
201
|
+
{ selectedRange: null },
|
|
202
|
+
);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
private formatSelectedRange(): string {
|
|
206
|
+
if (!this.selectedRange) return '';
|
|
207
|
+
const formatter = new Intl.DateTimeFormat(undefined, {
|
|
208
|
+
dateStyle: 'medium',
|
|
209
|
+
timeStyle: 'short',
|
|
210
|
+
});
|
|
211
|
+
return `Showing emails from ${formatter.format(this.selectedRange.from)} to ${formatter.format(this.selectedRange.to)}`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private getEmailTrafficSeries(): Array<{
|
|
215
|
+
name: string;
|
|
216
|
+
color: string;
|
|
217
|
+
data: Array<{ x: number; y: number }>;
|
|
218
|
+
}> {
|
|
219
|
+
if (!this.traffic) return [];
|
|
220
|
+
return [
|
|
221
|
+
{
|
|
222
|
+
name: 'Sent',
|
|
223
|
+
color: '#22c55e',
|
|
224
|
+
data: this.traffic.sent.map((point) => ({ x: point.timestamp, y: point.value })),
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
name: 'Received',
|
|
228
|
+
color: '#3b82f6',
|
|
229
|
+
data: this.traffic.received.map((point) => ({ x: point.timestamp, y: point.value })),
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
name: 'Failed',
|
|
233
|
+
color: '#ef4444',
|
|
234
|
+
data: this.traffic.failed.map((point) => ({ x: point.timestamp, y: point.value })),
|
|
235
|
+
},
|
|
236
|
+
];
|
|
237
|
+
}
|
|
238
|
+
|
|
111
239
|
private handleBack() {
|
|
112
240
|
this.detailRequestGeneration++;
|
|
113
241
|
this.selectedEmail = null;
|
package/ts_web/readme.md
CHANGED
|
@@ -27,6 +27,13 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|
|
27
27
|
| `security` | `overview`, `blocked`, `authentication` |
|
|
28
28
|
| `domains` | `providers`, `domains`, `dns`, `certificates` |
|
|
29
29
|
|
|
30
|
+
The Email Log keeps its search controls visible while scrolling and places the
|
|
31
|
+
traffic chart above the retained email table. With no search the chart shows the
|
|
32
|
+
last 24 hours. A search filters both the table and chart, expanding the chart to
|
|
33
|
+
7, 14, or 30 days when older matching mail exists. Email Security lists retained
|
|
34
|
+
SPF, DKIM, and enforcing DMARC findings with sender, recipient, subject, and a
|
|
35
|
+
concise reason.
|
|
36
|
+
|
|
30
37
|
## Routes Navigation
|
|
31
38
|
|
|
32
39
|
The `/network/routes` view uses two adjacent, independent multitoggles:
|