@aglyn/plugins-email 1.0.0-beta.143
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/LICENSE +201 -0
- package/README.md +7 -0
- package/package.json +58 -0
- package/src/index.d.ts +35 -0
- package/src/index.js +35 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/campaign-design-create-widget.d.ts +21 -0
- package/src/lib/components/campaign-design-create-widget.js +61 -0
- package/src/lib/components/campaign-design-create-widget.js.map +1 -0
- package/src/lib/components/campaign-sender-editor-widget.d.ts +18 -0
- package/src/lib/components/campaign-sender-editor-widget.js +39 -0
- package/src/lib/components/campaign-sender-editor-widget.js.map +1 -0
- package/src/lib/components/campaign-topic-options-widget.d.ts +19 -0
- package/src/lib/components/campaign-topic-options-widget.js +48 -0
- package/src/lib/components/campaign-topic-options-widget.js.map +1 -0
- package/src/lib/components/campaign-topic-select.d.ts +32 -0
- package/src/lib/components/campaign-topic-select.js +87 -0
- package/src/lib/components/campaign-topic-select.js.map +1 -0
- package/src/lib/components/dynamic-list-rule-fields.d.ts +173 -0
- package/src/lib/components/dynamic-list-rule-fields.js +1473 -0
- package/src/lib/components/dynamic-list-rule-fields.js.map +1 -0
- package/src/lib/components/email-blocks.d.ts +111 -0
- package/src/lib/components/email-blocks.js +875 -0
- package/src/lib/components/email-blocks.js.map +1 -0
- package/src/lib/components/email-design-preview.d.ts +62 -0
- package/src/lib/components/email-design-preview.js +174 -0
- package/src/lib/components/email-design-preview.js.map +1 -0
- package/src/lib/components/email-screens-card.d.ts +42 -0
- package/src/lib/components/email-screens-card.js +277 -0
- package/src/lib/components/email-screens-card.js.map +1 -0
- package/src/lib/components/email-template-detail.d.ts +48 -0
- package/src/lib/components/email-template-detail.js +681 -0
- package/src/lib/components/email-template-detail.js.map +1 -0
- package/src/lib/components/email-topic-detail.d.ts +32 -0
- package/src/lib/components/email-topic-detail.js +293 -0
- package/src/lib/components/email-topic-detail.js.map +1 -0
- package/src/lib/components/email-topics-card.d.ts +46 -0
- package/src/lib/components/email-topics-card.js +327 -0
- package/src/lib/components/email-topics-card.js.map +1 -0
- package/src/lib/components/email-zones.d.ts +28 -0
- package/src/lib/components/email-zones.js +20 -0
- package/src/lib/components/email-zones.js.map +1 -0
- package/src/lib/components/emails-console-page.d.ts +32 -0
- package/src/lib/components/emails-console-page.js +229 -0
- package/src/lib/components/emails-console-page.js.map +1 -0
- package/src/lib/components/emails-console-sections.d.ts +36 -0
- package/src/lib/components/emails-console-sections.js +108 -0
- package/src/lib/components/emails-console-sections.js.map +1 -0
- package/src/lib/components/list-detail-card.d.ts +47 -0
- package/src/lib/components/list-detail-card.js +273 -0
- package/src/lib/components/list-detail-card.js.map +1 -0
- package/src/lib/components/list-edit-card.d.ts +11 -0
- package/src/lib/components/list-edit-card.js +287 -0
- package/src/lib/components/list-edit-card.js.map +1 -0
- package/src/lib/components/list-import-drawer.d.ts +22 -0
- package/src/lib/components/list-import-drawer.js +662 -0
- package/src/lib/components/list-import-drawer.js.map +1 -0
- package/src/lib/components/list-members-panel.d.ts +94 -0
- package/src/lib/components/list-members-panel.js +686 -0
- package/src/lib/components/list-members-panel.js.map +1 -0
- package/src/lib/components/lists-card.d.ts +28 -0
- package/src/lib/components/lists-card.js +377 -0
- package/src/lib/components/lists-card.js.map +1 -0
- package/src/lib/components/sending-domain-detail.d.ts +26 -0
- package/src/lib/components/sending-domain-detail.js +496 -0
- package/src/lib/components/sending-domain-detail.js.map +1 -0
- package/src/lib/components/sending-domains-card.d.ts +33 -0
- package/src/lib/components/sending-domains-card.js +962 -0
- package/src/lib/components/sending-domains-card.js.map +1 -0
- package/src/lib/components/sending-sender-drawer.d.ts +94 -0
- package/src/lib/components/sending-sender-drawer.js +543 -0
- package/src/lib/components/sending-sender-drawer.js.map +1 -0
- package/src/lib/components/suppressions-card.d.ts +49 -0
- package/src/lib/components/suppressions-card.js +639 -0
- package/src/lib/components/suppressions-card.js.map +1 -0
- package/src/lib/components/use-org-email-topics.d.ts +79 -0
- package/src/lib/components/use-org-email-topics.js +111 -0
- package/src/lib/components/use-org-email-topics.js.map +1 -0
- package/src/lib/constants/bundle-common.d.ts +18 -0
- package/src/lib/constants/bundle-common.js +18 -0
- package/src/lib/constants/bundle-common.js.map +1 -0
- package/src/lib/hooks/use-org-company-options.d.ts +20 -0
- package/src/lib/hooks/use-org-company-options.js +138 -0
- package/src/lib/hooks/use-org-company-options.js.map +1 -0
- package/src/lib/hooks/use-org-contact-fields.d.ts +40 -0
- package/src/lib/hooks/use-org-contact-fields.js +91 -0
- package/src/lib/hooks/use-org-contact-fields.js.map +1 -0
- package/src/lib/hooks/use-org-contact-segments.d.ts +16 -0
- package/src/lib/hooks/use-org-contact-segments.js +55 -0
- package/src/lib/hooks/use-org-contact-segments.js.map +1 -0
- package/src/lib/hooks/use-org-crm-views.d.ts +8 -0
- package/src/lib/hooks/use-org-crm-views.js +74 -0
- package/src/lib/hooks/use-org-crm-views.js.map +1 -0
- package/src/lib/hooks/use-org-lists.d.ts +8 -0
- package/src/lib/hooks/use-org-lists.js +47 -0
- package/src/lib/hooks/use-org-lists.js.map +1 -0
- package/src/lib/model/email-design-document.d.ts +52 -0
- package/src/lib/model/email-design-document.js +62 -0
- package/src/lib/model/email-design-document.js.map +1 -0
- package/src/lib/model/index.d.ts +64 -0
- package/src/lib/model/index.js +71 -0
- package/src/lib/model/index.js.map +1 -0
- package/src/lib/model/sending-domain-status.d.ts +99 -0
- package/src/lib/model/sending-domain-status.js +196 -0
- package/src/lib/model/sending-domain-status.js.map +1 -0
- package/src/lib/model/template-provenance.d.ts +113 -0
- package/src/lib/model/template-provenance.js +107 -0
- package/src/lib/model/template-provenance.js.map +1 -0
- package/src/lib/model/template-report.d.ts +158 -0
- package/src/lib/model/template-report.js +249 -0
- package/src/lib/model/template-report.js.map +1 -0
- package/src/lib/plugin.d.ts +27 -0
- package/src/lib/plugin.js +163 -0
- package/src/lib/plugin.js.map +1 -0
- package/src/lib/server-console.d.ts +116 -0
- package/src/lib/server-console.js +422 -0
- package/src/lib/server-console.js.map +1 -0
- package/src/lib/server-email-drafts.d.ts +104 -0
- package/src/lib/server-email-drafts.js +381 -0
- package/src/lib/server-email-drafts.js.map +1 -0
- package/src/lib/server-list-gate.d.ts +183 -0
- package/src/lib/server-list-gate.js +365 -0
- package/src/lib/server-list-gate.js.map +1 -0
- package/src/lib/server-list-import.d.ts +199 -0
- package/src/lib/server-list-import.js +632 -0
- package/src/lib/server-list-import.js.map +1 -0
- package/src/lib/server-suppressions.d.ts +135 -0
- package/src/lib/server-suppressions.js +295 -0
- package/src/lib/server-suppressions.js.map +1 -0
- package/src/lib/server.d.ts +19 -0
- package/src/lib/server.js +834 -0
- package/src/lib/server.js.map +1 -0
- package/src/lib/site.d.ts +26 -0
- package/src/lib/site.js +81 -0
- package/src/lib/site.js.map +1 -0
- package/src/lib/unsubscribe-link.d.ts +311 -0
- package/src/lib/unsubscribe-link.js +398 -0
- package/src/lib/unsubscribe-link.js.map +1 -0
- package/src/lib/utils/create-email-screen.d.ts +59 -0
- package/src/lib/utils/create-email-screen.js +59 -0
- package/src/lib/utils/create-email-screen.js.map +1 -0
- package/src/lib/utils/generate-preset-id.d.ts +19 -0
- package/src/lib/utils/generate-preset-id.js +25 -0
- package/src/lib/utils/generate-preset-id.js.map +1 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
19
|
+
import { PageHeaderRecord, pluginDocsHelp } from "@aglyn/aglyn";
|
|
20
|
+
import { mdiDeleteOutline, mdiEmailCheckOutline } from "@aglyn/shared-data-mdi";
|
|
21
|
+
import { CardDisplay, MdiIcon, useConfirmationContext } from "@aglyn/shared-ui-jsx";
|
|
22
|
+
import RowActionsMenu from "@aglyn/shared-ui-jsx/components/row-actions-menu.component";
|
|
23
|
+
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
24
|
+
import { Alert, Button, Chip, Divider, Stack, Typography } from "@mui/material";
|
|
25
|
+
import { useRouter } from "next/navigation";
|
|
26
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
27
|
+
import { describeSendingDomain, describeSendingDomainRemoval, INCONCLUSIVE_CHECK } from "../model/sending-domain-status.js";
|
|
28
|
+
import { useSendingApi } from "@aglyn/tenant-feature-instance/hooks/use-sending-identity-api";
|
|
29
|
+
/** The record block, in the shape a registrar's own form asks for. */ function RecordRow(props) {
|
|
30
|
+
const { record, missing } = props;
|
|
31
|
+
const target = record.priority ? `${record.priority} ${record.value}` : record.value;
|
|
32
|
+
return /*#__PURE__*/ _jsxs(Stack, {
|
|
33
|
+
spacing: 0.5,
|
|
34
|
+
children: [
|
|
35
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
36
|
+
direction: "row",
|
|
37
|
+
spacing: 1,
|
|
38
|
+
sx: {
|
|
39
|
+
alignItems: 'center'
|
|
40
|
+
},
|
|
41
|
+
children: [
|
|
42
|
+
/*#__PURE__*/ _jsx(Chip, {
|
|
43
|
+
size: "small",
|
|
44
|
+
label: record.type,
|
|
45
|
+
variant: "outlined"
|
|
46
|
+
}),
|
|
47
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
48
|
+
variant: "body2",
|
|
49
|
+
sx: {
|
|
50
|
+
fontWeight: 'bold'
|
|
51
|
+
},
|
|
52
|
+
children: record.purpose === 'spf' ? 'Authorization' : record.purpose === 'dkim' ? 'Signing key' : record.purpose === 'return-path' ? 'Bounce routing' : 'DMARC policy'
|
|
53
|
+
}),
|
|
54
|
+
record.required ? null : /*#__PURE__*/ _jsx(Chip, {
|
|
55
|
+
size: "small",
|
|
56
|
+
label: "Optional",
|
|
57
|
+
variant: "outlined"
|
|
58
|
+
}),
|
|
59
|
+
missing ? /*#__PURE__*/ _jsx(Chip, {
|
|
60
|
+
size: "small",
|
|
61
|
+
label: "Not seen yet",
|
|
62
|
+
color: "warning"
|
|
63
|
+
}) : null
|
|
64
|
+
]
|
|
65
|
+
}),
|
|
66
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
67
|
+
variant: "body2",
|
|
68
|
+
component: "code",
|
|
69
|
+
sx: {
|
|
70
|
+
p: 1,
|
|
71
|
+
bgcolor: 'action.hover',
|
|
72
|
+
borderRadius: 1,
|
|
73
|
+
fontFamily: 'monospace',
|
|
74
|
+
whiteSpace: 'pre-wrap',
|
|
75
|
+
overflowWrap: 'anywhere'
|
|
76
|
+
},
|
|
77
|
+
children: `${record.type} ${record.name} → ${target || '(not issued yet)'}`
|
|
78
|
+
}),
|
|
79
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
80
|
+
variant: "body2",
|
|
81
|
+
color: "text.secondary",
|
|
82
|
+
children: record.note
|
|
83
|
+
})
|
|
84
|
+
]
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
RecordRow.displayName = 'RecordRow';
|
|
88
|
+
/**
|
|
89
|
+
* ONE SENDING DOMAIN: what to publish, whether we can see it, and whether
|
|
90
|
+
* this site sends as it.
|
|
91
|
+
*
|
|
92
|
+
* ## `inconclusive` is not a failure, and this page is where that is felt
|
|
93
|
+
*
|
|
94
|
+
* Pressing Check DNS has three outcomes, not two. The third is that nobody
|
|
95
|
+
* answered — a resolver outage, a timeout — and the route says so with a
|
|
96
|
+
* `503` rather than with `verified: false`. It is held HERE, in the surface's
|
|
97
|
+
* own state, next to a status the record still holds, because the record was
|
|
98
|
+
* not changed and saying otherwise would send a customer whose DNS is perfect
|
|
99
|
+
* to go and edit a zone that has nothing wrong with it.
|
|
100
|
+
*
|
|
101
|
+
* That is why `lastCheck` is a separate piece of state from `record.status`
|
|
102
|
+
* and never assigned into it.
|
|
103
|
+
*/ export function SendingDomainDetail(props) {
|
|
104
|
+
var _ref, _ref1, _ref2, _ref3, _ref4;
|
|
105
|
+
var _record_records, _record_dmarc;
|
|
106
|
+
const { hostId, domain, basePath } = props;
|
|
107
|
+
const call = useSendingApi();
|
|
108
|
+
const router = useRouter();
|
|
109
|
+
const { enqueueSnackbar } = useSnackbar();
|
|
110
|
+
/*
|
|
111
|
+
* The snackbar held in a REF, for the reason `use-campaign-send-api` holds
|
|
112
|
+
* the user in one: `load` is a dependency of the effect that runs it, so
|
|
113
|
+
* anything whose identity changes per render re-runs the fetch — and a
|
|
114
|
+
* hook returning a fresh object each render turns that into a loop that
|
|
115
|
+
* never settles. Reading it through a ref makes the effect depend on the
|
|
116
|
+
* things that actually identify the request.
|
|
117
|
+
*/ const notifyRef = useRef(enqueueSnackbar);
|
|
118
|
+
notifyRef.current = enqueueSnackbar;
|
|
119
|
+
const { confirm } = useConfirmationContext();
|
|
120
|
+
const [identity, setIdentity] = useState(null);
|
|
121
|
+
const [record, setRecord] = useState(null);
|
|
122
|
+
const [loading, setLoading] = useState(true);
|
|
123
|
+
const [busy, setBusy] = useState(false);
|
|
124
|
+
/**
|
|
125
|
+
* The outcome of the last check the reader ran, and ONLY when it was
|
|
126
|
+
* inconclusive. Never merged into the record: an unanswered lookup is
|
|
127
|
+
* evidence of nothing, and the record's own status is still the last thing
|
|
128
|
+
* we actually established.
|
|
129
|
+
*/ const [unreachable, setUnreachable] = useState(false);
|
|
130
|
+
const load = useCallback(async ()=>{
|
|
131
|
+
var _ref, _ref1;
|
|
132
|
+
var _view_payload, _domains_payload;
|
|
133
|
+
setLoading(true);
|
|
134
|
+
const view = await call({
|
|
135
|
+
path: 'sending-identity',
|
|
136
|
+
method: 'GET',
|
|
137
|
+
query: {
|
|
138
|
+
hostId
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
if (!view.response.ok) {
|
|
142
|
+
var _ref2;
|
|
143
|
+
var _view_payload1;
|
|
144
|
+
setLoading(false);
|
|
145
|
+
return void notifyRef.current((_ref2 = (_view_payload1 = view.payload) == null ? void 0 : _view_payload1.error) != null ? _ref2 : 'Could not read the sending identity', {
|
|
146
|
+
variant: 'warning'
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
setIdentity(view.payload);
|
|
150
|
+
const orgId = String((_ref = (_view_payload = view.payload) == null ? void 0 : _view_payload.orgId) != null ? _ref : '');
|
|
151
|
+
const domains = orgId ? await call({
|
|
152
|
+
path: 'sending-domains',
|
|
153
|
+
method: 'GET',
|
|
154
|
+
query: {
|
|
155
|
+
orgId
|
|
156
|
+
}
|
|
157
|
+
}) : null;
|
|
158
|
+
setLoading(false);
|
|
159
|
+
const found = ((_ref1 = domains == null ? void 0 : (_domains_payload = domains.payload) == null ? void 0 : _domains_payload.domains) != null ? _ref1 : []).find((one)=>one.domain === domain);
|
|
160
|
+
setRecord(found != null ? found : null);
|
|
161
|
+
}, [
|
|
162
|
+
call,
|
|
163
|
+
hostId,
|
|
164
|
+
domain
|
|
165
|
+
]);
|
|
166
|
+
useEffect(()=>{
|
|
167
|
+
void load();
|
|
168
|
+
}, [
|
|
169
|
+
load
|
|
170
|
+
]);
|
|
171
|
+
const handleVerify = useCallback(async ()=>{
|
|
172
|
+
if (busy || !(identity == null ? void 0 : identity.orgId)) return;
|
|
173
|
+
setBusy(true);
|
|
174
|
+
setUnreachable(false);
|
|
175
|
+
const { response, payload } = await call({
|
|
176
|
+
path: 'sending-domains',
|
|
177
|
+
method: 'POST',
|
|
178
|
+
body: {
|
|
179
|
+
orgId: identity.orgId,
|
|
180
|
+
domain,
|
|
181
|
+
action: 'verify'
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
setBusy(false);
|
|
185
|
+
/*
|
|
186
|
+
* THE THIRD OUTCOME.
|
|
187
|
+
*
|
|
188
|
+
* A `503` here means the lookup got no answer. Nothing about the domain
|
|
189
|
+
* changed, so nothing on screen changes either except a notice saying the
|
|
190
|
+
* question could not be asked. Treating it as a failed check — which is
|
|
191
|
+
* what a plain `!response.ok` branch would do — is how a customer ends up
|
|
192
|
+
* rewriting records that were already correct.
|
|
193
|
+
*/ if (response.status === 503) {
|
|
194
|
+
setUnreachable(true);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (!response.ok) {
|
|
198
|
+
var _ref;
|
|
199
|
+
return void notifyRef.current((_ref = payload == null ? void 0 : payload.error) != null ? _ref : 'The check could not run', {
|
|
200
|
+
variant: 'warning'
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
await load();
|
|
204
|
+
notifyRef.current((payload == null ? void 0 : payload.verified) ? 'Verified — this domain can send' : 'Checked. Some records are still missing.', {
|
|
205
|
+
variant: (payload == null ? void 0 : payload.verified) ? 'success' : 'info'
|
|
206
|
+
});
|
|
207
|
+
}, [
|
|
208
|
+
busy,
|
|
209
|
+
call,
|
|
210
|
+
identity == null ? void 0 : identity.orgId,
|
|
211
|
+
domain,
|
|
212
|
+
load
|
|
213
|
+
]);
|
|
214
|
+
const handleRequestRecords = useCallback(async ()=>{
|
|
215
|
+
if (busy || !(identity == null ? void 0 : identity.orgId)) return;
|
|
216
|
+
setBusy(true);
|
|
217
|
+
const { response, payload } = await call({
|
|
218
|
+
path: 'sending-domains',
|
|
219
|
+
method: 'POST',
|
|
220
|
+
body: {
|
|
221
|
+
orgId: identity.orgId,
|
|
222
|
+
domain,
|
|
223
|
+
action: 'request'
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
setBusy(false);
|
|
227
|
+
if (!response.ok) {
|
|
228
|
+
var _ref;
|
|
229
|
+
return void notifyRef.current((_ref = payload == null ? void 0 : payload.error) != null ? _ref : 'Could not request records', {
|
|
230
|
+
variant: 'warning'
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
await load();
|
|
234
|
+
}, [
|
|
235
|
+
busy,
|
|
236
|
+
call,
|
|
237
|
+
identity == null ? void 0 : identity.orgId,
|
|
238
|
+
domain,
|
|
239
|
+
load
|
|
240
|
+
]);
|
|
241
|
+
const handleUse = useCallback(async (next)=>{
|
|
242
|
+
if (busy) return;
|
|
243
|
+
setBusy(true);
|
|
244
|
+
const { response, payload } = await call({
|
|
245
|
+
path: 'sending-identity',
|
|
246
|
+
method: 'POST',
|
|
247
|
+
body: {
|
|
248
|
+
hostId,
|
|
249
|
+
domain: next
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
setBusy(false);
|
|
253
|
+
if (!response.ok) {
|
|
254
|
+
var _ref;
|
|
255
|
+
return void notifyRef.current((_ref = payload == null ? void 0 : payload.error) != null ? _ref : 'Could not change the identity', {
|
|
256
|
+
variant: 'warning'
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
await load();
|
|
260
|
+
/*
|
|
261
|
+
* The address the ROUTE settled on, not the one this click asked for.
|
|
262
|
+
*
|
|
263
|
+
* Clearing the selection does not name a domain — it hands the site back
|
|
264
|
+
* to the one provisioned for it — so only the response knows what the
|
|
265
|
+
* `From:` line now says. A message composed from `next` would announce
|
|
266
|
+
* an address chosen here rather than the one recipients will see.
|
|
267
|
+
*/ notifyRef.current((payload == null ? void 0 : payload.from) ? `This site now sends as ${payload.from}` : 'This site’s sending address was changed', {
|
|
268
|
+
variant: 'success'
|
|
269
|
+
});
|
|
270
|
+
}, [
|
|
271
|
+
busy,
|
|
272
|
+
call,
|
|
273
|
+
hostId,
|
|
274
|
+
load
|
|
275
|
+
]);
|
|
276
|
+
const handleRelease = useCallback(async ()=>{
|
|
277
|
+
if (!(identity == null ? void 0 : identity.orgId)) return;
|
|
278
|
+
/*
|
|
279
|
+
* The consequence stated rather than implied, and it is not one
|
|
280
|
+
* consequence — see `describeSendingDomainRemoval`, which holds all three.
|
|
281
|
+
*
|
|
282
|
+
* Taken from the model rather than written here because the LIST asks to
|
|
283
|
+
* remove the same record from its row menu. Two confirmations describing
|
|
284
|
+
* one action differently is how a merchant learns to dismiss the harsher
|
|
285
|
+
* of them.
|
|
286
|
+
*/ const ok = await confirm(_extends({}, describeSendingDomainRemoval({
|
|
287
|
+
domain,
|
|
288
|
+
selected: identity.selected,
|
|
289
|
+
platformDomain: identity.platformDomain
|
|
290
|
+
}), {
|
|
291
|
+
confirmationButtonProps: {
|
|
292
|
+
color: 'error'
|
|
293
|
+
}
|
|
294
|
+
}))// `confirm` resolves with no value and REJECTS on cancel, so the
|
|
295
|
+
// resolved value alone can never gate this.
|
|
296
|
+
.then(()=>true).catch(()=>false);
|
|
297
|
+
if (!ok) return;
|
|
298
|
+
const { response, payload } = await call({
|
|
299
|
+
path: 'sending-domains',
|
|
300
|
+
method: 'DELETE',
|
|
301
|
+
query: {
|
|
302
|
+
orgId: identity.orgId,
|
|
303
|
+
domain
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
if (!response.ok) {
|
|
307
|
+
var _ref;
|
|
308
|
+
return void notifyRef.current((_ref = payload == null ? void 0 : payload.error) != null ? _ref : 'Could not remove the domain', {
|
|
309
|
+
variant: 'warning'
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
router.push(`${basePath}/sending`);
|
|
313
|
+
}, [
|
|
314
|
+
confirm,
|
|
315
|
+
call,
|
|
316
|
+
identity,
|
|
317
|
+
domain,
|
|
318
|
+
router,
|
|
319
|
+
basePath
|
|
320
|
+
]);
|
|
321
|
+
const state = describeSendingDomain({
|
|
322
|
+
status: (_ref = record == null ? void 0 : record.status) != null ? _ref : 'requested',
|
|
323
|
+
pendingProvider: (_ref1 = record == null ? void 0 : record.pendingProvider) != null ? _ref1 : (record == null ? void 0 : record.status) === 'requested',
|
|
324
|
+
issueError: (_ref2 = record == null ? void 0 : record.lastIssueError) != null ? _ref2 : null,
|
|
325
|
+
missing: (_ref3 = record == null ? void 0 : record.lastMissing) != null ? _ref3 : null
|
|
326
|
+
});
|
|
327
|
+
const missing = new Set((_ref4 = record == null ? void 0 : record.lastMissing) != null ? _ref4 : []);
|
|
328
|
+
const canManage = (identity == null ? void 0 : identity.canManage) === true;
|
|
329
|
+
const isSelected = (identity == null ? void 0 : identity.selected) === domain;
|
|
330
|
+
const actions = canManage ? [
|
|
331
|
+
{
|
|
332
|
+
key: 'release',
|
|
333
|
+
label: 'Remove domain',
|
|
334
|
+
icon: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
335
|
+
path: mdiDeleteOutline.path,
|
|
336
|
+
size: 0.8
|
|
337
|
+
}),
|
|
338
|
+
destructive: true,
|
|
339
|
+
onClick: ()=>void handleRelease()
|
|
340
|
+
}
|
|
341
|
+
] : [];
|
|
342
|
+
/* The card, named so the page chrome above it is a plain list of
|
|
343
|
+
what this surface publishes upward. */ const card = /*#__PURE__*/ _jsx(CardDisplay, {
|
|
344
|
+
header: 'Sending domain',
|
|
345
|
+
help: pluginDocsHelp('emailCampaigns', {
|
|
346
|
+
anchor: '#sending-domains'
|
|
347
|
+
}),
|
|
348
|
+
contentGutterX: true,
|
|
349
|
+
contentGutterY: true,
|
|
350
|
+
contentBordered: "all",
|
|
351
|
+
HeaderProps: {
|
|
352
|
+
action: actions.length ? /*#__PURE__*/ _jsx(RowActionsMenu, {
|
|
353
|
+
items: actions
|
|
354
|
+
}) : null
|
|
355
|
+
},
|
|
356
|
+
children: loading ? /*#__PURE__*/ _jsx(Typography, {
|
|
357
|
+
variant: "body2",
|
|
358
|
+
color: "text.secondary",
|
|
359
|
+
children: 'Loading…'
|
|
360
|
+
}) : !record ? /*#__PURE__*/ _jsx(Alert, {
|
|
361
|
+
severity: "warning",
|
|
362
|
+
children: `${domain} is not claimed by this workspace. It may have been ` + `removed — go back and add it again if you still want it.`
|
|
363
|
+
}) : /*#__PURE__*/ _jsxs(Stack, {
|
|
364
|
+
spacing: 2,
|
|
365
|
+
children: [
|
|
366
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
367
|
+
direction: "row",
|
|
368
|
+
spacing: 1,
|
|
369
|
+
sx: {
|
|
370
|
+
alignItems: 'center'
|
|
371
|
+
},
|
|
372
|
+
children: [
|
|
373
|
+
/*#__PURE__*/ _jsx(Chip, {
|
|
374
|
+
label: state.label,
|
|
375
|
+
color: state.color,
|
|
376
|
+
variant: state.sending ? 'filled' : 'outlined'
|
|
377
|
+
}),
|
|
378
|
+
isSelected ? /*#__PURE__*/ _jsx(Chip, {
|
|
379
|
+
label: "This site sends as this",
|
|
380
|
+
color: "primary",
|
|
381
|
+
size: "small"
|
|
382
|
+
}) : null
|
|
383
|
+
]
|
|
384
|
+
}),
|
|
385
|
+
/*#__PURE__*/ _jsx(Alert, {
|
|
386
|
+
severity: state.severity,
|
|
387
|
+
children: state.text
|
|
388
|
+
}),
|
|
389
|
+
unreachable ? /*#__PURE__*/ _jsxs(Alert, {
|
|
390
|
+
severity: INCONCLUSIVE_CHECK.severity,
|
|
391
|
+
icon: false,
|
|
392
|
+
children: [
|
|
393
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
394
|
+
variant: "body2",
|
|
395
|
+
sx: {
|
|
396
|
+
fontWeight: 'bold'
|
|
397
|
+
},
|
|
398
|
+
children: INCONCLUSIVE_CHECK.label
|
|
399
|
+
}),
|
|
400
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
401
|
+
variant: "body2",
|
|
402
|
+
children: INCONCLUSIVE_CHECK.text
|
|
403
|
+
})
|
|
404
|
+
]
|
|
405
|
+
}) : null,
|
|
406
|
+
record.dmarc ? /*#__PURE__*/ _jsx(Alert, {
|
|
407
|
+
severity: record.dmarc.policy === 'reject' && record.status !== 'verified' ? 'error' : 'info',
|
|
408
|
+
children: record.dmarc.consequence
|
|
409
|
+
}) : null,
|
|
410
|
+
((_record_records = record.records) == null ? void 0 : _record_records.length) && record.records.some((entry)=>entry.value) ? /*#__PURE__*/ _jsxs(_Fragment, {
|
|
411
|
+
children: [
|
|
412
|
+
/*#__PURE__*/ _jsx(Divider, {}),
|
|
413
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
414
|
+
variant: "body2",
|
|
415
|
+
sx: {
|
|
416
|
+
fontWeight: 'bold'
|
|
417
|
+
},
|
|
418
|
+
children: 'Publish these records'
|
|
419
|
+
}),
|
|
420
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
421
|
+
spacing: 2,
|
|
422
|
+
children: [
|
|
423
|
+
record.records.filter((entry)=>entry.value).map((entry)=>/*#__PURE__*/ _jsx(RecordRow, {
|
|
424
|
+
record: entry,
|
|
425
|
+
missing: missing.has(`${entry.type}:${entry.name}`)
|
|
426
|
+
}, `${entry.type}:${entry.name}`)),
|
|
427
|
+
record.dmarcSuggestion && ((_record_dmarc = record.dmarc) == null ? void 0 : _record_dmarc.policy) === 'absent' ? /*#__PURE__*/ _jsx(RecordRow, {
|
|
428
|
+
record: record.dmarcSuggestion,
|
|
429
|
+
missing: false
|
|
430
|
+
}) : null
|
|
431
|
+
]
|
|
432
|
+
})
|
|
433
|
+
]
|
|
434
|
+
}) : null,
|
|
435
|
+
/*#__PURE__*/ _jsx(Divider, {}),
|
|
436
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
437
|
+
direction: "row",
|
|
438
|
+
spacing: 1,
|
|
439
|
+
sx: {
|
|
440
|
+
flexWrap: 'wrap',
|
|
441
|
+
gap: 1
|
|
442
|
+
},
|
|
443
|
+
children: [
|
|
444
|
+
record.status === 'requested' && canManage ? /*#__PURE__*/ _jsx(Button, {
|
|
445
|
+
variant: "contained",
|
|
446
|
+
disabled: busy,
|
|
447
|
+
onClick: ()=>void handleRequestRecords(),
|
|
448
|
+
children: busy ? 'Working…' : 'Request records'
|
|
449
|
+
}) : null,
|
|
450
|
+
record.status !== 'requested' ? /*#__PURE__*/ _jsx(Button, {
|
|
451
|
+
variant: "contained",
|
|
452
|
+
disabled: busy,
|
|
453
|
+
onClick: ()=>void handleVerify(),
|
|
454
|
+
children: busy ? 'Checking…' : 'Check DNS'
|
|
455
|
+
}) : null,
|
|
456
|
+
canManage && record.status === 'verified' && !isSelected ? /*#__PURE__*/ _jsx(Button, {
|
|
457
|
+
variant: "outlined",
|
|
458
|
+
startIcon: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
459
|
+
path: mdiEmailCheckOutline.path,
|
|
460
|
+
size: 0.8
|
|
461
|
+
}),
|
|
462
|
+
disabled: busy,
|
|
463
|
+
onClick: ()=>void handleUse(domain),
|
|
464
|
+
children: 'Send this site’s email as this domain'
|
|
465
|
+
}) : null,
|
|
466
|
+
canManage && isSelected ? /*
|
|
467
|
+
* Clearing the selection returns the site to the domain
|
|
468
|
+
* PROVISIONED for it — `{label}.mail.aglyn.app` — not to the
|
|
469
|
+
* shared pool, and the label says so. A site whose plan carries
|
|
470
|
+
* no such domain is refused here with the reason, which is why
|
|
471
|
+
* the control is worded as leaving this domain rather than
|
|
472
|
+
* promising a destination it may not reach.
|
|
473
|
+
*/ /*#__PURE__*/ _jsx(Button, {
|
|
474
|
+
variant: "outlined",
|
|
475
|
+
disabled: busy,
|
|
476
|
+
onClick: ()=>void handleUse('platform'),
|
|
477
|
+
children: 'Stop sending this site’s email as this domain'
|
|
478
|
+
}) : null
|
|
479
|
+
]
|
|
480
|
+
})
|
|
481
|
+
]
|
|
482
|
+
})
|
|
483
|
+
});
|
|
484
|
+
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
485
|
+
children: [
|
|
486
|
+
/*#__PURE__*/ _jsx(PageHeaderRecord, {
|
|
487
|
+
title: domain
|
|
488
|
+
}),
|
|
489
|
+
card
|
|
490
|
+
]
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
SendingDomainDetail.displayName = 'SendingDomainDetail';
|
|
494
|
+
export default SendingDomainDetail;
|
|
495
|
+
|
|
496
|
+
//# sourceMappingURL=sending-domain-detail.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/components/sending-domain-detail.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { PageHeaderRecord, pluginDocsHelp } from '@aglyn/aglyn'\nimport { mdiDeleteOutline, mdiEmailCheckOutline } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport RowActionsMenu, {\n type RowActionsMenuItem,\n} from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Alert,\n Button,\n Chip,\n Divider,\n Stack,\n Typography,\n} from '@mui/material'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport {\n describeSendingDomain,\n describeSendingDomainRemoval,\n INCONCLUSIVE_CHECK,\n} from '../model/sending-domain-status'\nimport {\n useSendingApi,\n type SendingDnsRecordView,\n type SendingDomainView,\n type SendingIdentityView,\n} from '@aglyn/tenant-feature-instance/hooks/use-sending-identity-api'\n\nexport interface SendingDomainDetailProps {\n hostId: string\n domain: string\n basePath: string\n}\n\n/** The record block, in the shape a registrar's own form asks for. */\nfunction RecordRow(props: { record: SendingDnsRecordView; missing: boolean }) {\n const { record, missing } = props\n const target = record.priority\n ? `${record.priority} ${record.value}`\n : record.value\n return (\n <Stack spacing={0.5}>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Chip size=\"small\" label={record.type} variant=\"outlined\" />\n <Typography variant=\"body2\" sx={{ fontWeight: 'bold' }}>\n {record.purpose === 'spf'\n ? 'Authorization'\n : record.purpose === 'dkim'\n ? 'Signing key'\n : record.purpose === 'return-path'\n ? 'Bounce routing'\n : 'DMARC policy'}\n </Typography>\n {record.required ? null : (\n <Chip size=\"small\" label=\"Optional\" variant=\"outlined\" />\n )}\n {missing ? <Chip size=\"small\" label=\"Not seen yet\" color=\"warning\" /> : null}\n </Stack>\n <Typography\n variant=\"body2\"\n component=\"code\"\n sx={{\n p: 1,\n bgcolor: 'action.hover',\n borderRadius: 1,\n fontFamily: 'monospace',\n whiteSpace: 'pre-wrap',\n overflowWrap: 'anywhere',\n }}\n >\n {`${record.type} ${record.name} → ${target || '(not issued yet)'}`}\n </Typography>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {record.note}\n </Typography>\n </Stack>\n )\n}\nRecordRow.displayName = 'RecordRow'\n\n/**\n * ONE SENDING DOMAIN: what to publish, whether we can see it, and whether\n * this site sends as it.\n *\n * ## `inconclusive` is not a failure, and this page is where that is felt\n *\n * Pressing Check DNS has three outcomes, not two. The third is that nobody\n * answered — a resolver outage, a timeout — and the route says so with a\n * `503` rather than with `verified: false`. It is held HERE, in the surface's\n * own state, next to a status the record still holds, because the record was\n * not changed and saying otherwise would send a customer whose DNS is perfect\n * to go and edit a zone that has nothing wrong with it.\n *\n * That is why `lastCheck` is a separate piece of state from `record.status`\n * and never assigned into it.\n */\nexport function SendingDomainDetail(props: SendingDomainDetailProps) {\n const { hostId, domain, basePath } = props\n const call = useSendingApi()\n const router = useRouter()\n const { enqueueSnackbar } = useSnackbar()\n /*\n * The snackbar held in a REF, for the reason `use-campaign-send-api` holds\n * the user in one: `load` is a dependency of the effect that runs it, so\n * anything whose identity changes per render re-runs the fetch — and a\n * hook returning a fresh object each render turns that into a loop that\n * never settles. Reading it through a ref makes the effect depend on the\n * things that actually identify the request.\n */\n const notifyRef = useRef(enqueueSnackbar)\n notifyRef.current = enqueueSnackbar\n const { confirm } = useConfirmationContext()\n\n const [identity, setIdentity] = useState<SendingIdentityView | null>(null)\n const [record, setRecord] = useState<SendingDomainView | null>(null)\n const [loading, setLoading] = useState(true)\n const [busy, setBusy] = useState(false)\n /**\n * The outcome of the last check the reader ran, and ONLY when it was\n * inconclusive. Never merged into the record: an unanswered lookup is\n * evidence of nothing, and the record's own status is still the last thing\n * we actually established.\n */\n const [unreachable, setUnreachable] = useState(false)\n\n const load = useCallback(async () => {\n setLoading(true)\n const view = await call({\n path: 'sending-identity',\n method: 'GET',\n query: { hostId },\n })\n if (!view.response.ok) {\n setLoading(false)\n return void notifyRef.current(\n view.payload?.error ?? 'Could not read the sending identity',\n { variant: 'warning' },\n )\n }\n setIdentity(view.payload as SendingIdentityView)\n const orgId = String(view.payload?.orgId ?? '')\n const domains = orgId\n ? await call({\n path: 'sending-domains',\n method: 'GET',\n query: { orgId },\n })\n : null\n setLoading(false)\n const found = (domains?.payload?.domains ?? []).find(\n (one: SendingDomainView) => one.domain === domain,\n )\n setRecord(found ?? null)\n }, [call, hostId, domain])\n\n useEffect(() => {\n void load()\n }, [load])\n\n const handleVerify = useCallback(async () => {\n if (busy || !identity?.orgId) return\n setBusy(true)\n setUnreachable(false)\n const { response, payload } = await call({\n path: 'sending-domains',\n method: 'POST',\n body: { orgId: identity.orgId, domain, action: 'verify' },\n })\n setBusy(false)\n /*\n * THE THIRD OUTCOME.\n *\n * A `503` here means the lookup got no answer. Nothing about the domain\n * changed, so nothing on screen changes either except a notice saying the\n * question could not be asked. Treating it as a failed check — which is\n * what a plain `!response.ok` branch would do — is how a customer ends up\n * rewriting records that were already correct.\n */\n if (response.status === 503) {\n setUnreachable(true)\n return\n }\n if (!response.ok) {\n return void notifyRef.current(payload?.error ?? 'The check could not run', {\n variant: 'warning',\n })\n }\n await load()\n notifyRef.current(\n payload?.verified\n ? 'Verified — this domain can send'\n : 'Checked. Some records are still missing.',\n { variant: payload?.verified ? 'success' : 'info' },\n )\n }, [busy, call, identity?.orgId, domain, load])\n\n const handleRequestRecords = useCallback(async () => {\n if (busy || !identity?.orgId) return\n setBusy(true)\n const { response, payload } = await call({\n path: 'sending-domains',\n method: 'POST',\n body: { orgId: identity.orgId, domain, action: 'request' },\n })\n setBusy(false)\n if (!response.ok) {\n return void notifyRef.current(payload?.error ?? 'Could not request records', {\n variant: 'warning',\n })\n }\n await load()\n }, [busy, call, identity?.orgId, domain, load])\n\n const handleUse = useCallback(\n async (next: string) => {\n if (busy) return\n setBusy(true)\n const { response, payload } = await call({\n path: 'sending-identity',\n method: 'POST',\n body: { hostId, domain: next },\n })\n setBusy(false)\n if (!response.ok) {\n return void notifyRef.current(payload?.error ?? 'Could not change the identity', {\n variant: 'warning',\n })\n }\n await load()\n /*\n * The address the ROUTE settled on, not the one this click asked for.\n *\n * Clearing the selection does not name a domain — it hands the site back\n * to the one provisioned for it — so only the response knows what the\n * `From:` line now says. A message composed from `next` would announce\n * an address chosen here rather than the one recipients will see.\n */\n notifyRef.current(\n payload?.from\n ? `This site now sends as ${payload.from}`\n : 'This site’s sending address was changed',\n { variant: 'success' },\n )\n },\n [busy, call, hostId, load],\n )\n\n const handleRelease = useCallback(async () => {\n if (!identity?.orgId) return\n /*\n * The consequence stated rather than implied, and it is not one\n * consequence — see `describeSendingDomainRemoval`, which holds all three.\n *\n * Taken from the model rather than written here because the LIST asks to\n * remove the same record from its row menu. Two confirmations describing\n * one action differently is how a merchant learns to dismiss the harsher\n * of them.\n */\n const ok = await confirm({\n ...describeSendingDomainRemoval({\n domain,\n selected: identity.selected,\n platformDomain: identity.platformDomain,\n }),\n confirmationButtonProps: { color: 'error' },\n })\n // `confirm` resolves with no value and REJECTS on cancel, so the\n // resolved value alone can never gate this.\n .then(() => true)\n .catch(() => false)\n if (!ok) return\n const { response, payload } = await call({\n path: 'sending-domains',\n method: 'DELETE',\n query: { orgId: identity.orgId, domain },\n })\n if (!response.ok) {\n return void notifyRef.current(payload?.error ?? 'Could not remove the domain', {\n variant: 'warning',\n })\n }\n router.push(`${basePath}/sending`)\n }, [confirm, call, identity, domain, router, basePath])\n\n const state = describeSendingDomain({\n status: record?.status ?? 'requested',\n pendingProvider: record?.pendingProvider ?? record?.status === 'requested',\n issueError: record?.lastIssueError ?? null,\n missing: record?.lastMissing ?? null,\n })\n const missing = new Set(record?.lastMissing ?? [])\n const canManage = identity?.canManage === true\n const isSelected = identity?.selected === domain\n\n const actions: RowActionsMenuItem[] = canManage\n ? [\n {\n key: 'release',\n label: 'Remove domain',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n onClick: () => void handleRelease(),\n },\n ]\n : []\n\n /* The card, named so the page chrome above it is a plain list of\n what this surface publishes upward. */\n const card = (\n <CardDisplay\n header={'Sending domain'}\n help={pluginDocsHelp('emailCampaigns', {\n anchor: '#sending-domains',\n })}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n HeaderProps={{\n action: actions.length ? <RowActionsMenu items={actions} /> : null,\n }}\n >\n {loading ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Loading…'}\n </Typography>\n ) : !record ? (\n <Alert severity=\"warning\">\n {`${domain} is not claimed by this workspace. It may have been ` +\n `removed — go back and add it again if you still want it.`}\n </Alert>\n ) : (\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Chip\n label={state.label}\n color={state.color}\n variant={state.sending ? 'filled' : 'outlined'}\n />\n {isSelected ? (\n <Chip label=\"This site sends as this\" color=\"primary\" size=\"small\" />\n ) : null}\n </Stack>\n\n <Alert severity={state.severity}>{state.text}</Alert>\n\n {/*\n The unanswered check, BESIDE the state above and never instead of\n it. Both are true at once: the record still says what it said, and\n the last thing we tried could not be completed.\n */}\n {unreachable ? (\n <Alert severity={INCONCLUSIVE_CHECK.severity} icon={false}>\n <Typography variant=\"body2\" sx={{ fontWeight: 'bold' }}>\n {INCONCLUSIVE_CHECK.label}\n </Typography>\n <Typography variant=\"body2\">{INCONCLUSIVE_CHECK.text}</Typography>\n </Alert>\n ) : null}\n\n {/*\n The customer's DMARC policy, read and never written. It decides\n whether unauthenticated mail from this domain is filed as spam or\n refused outright, which is something a person publishing these\n records should know before they finish rather than after.\n */}\n {record.dmarc ? (\n <Alert\n severity={\n record.dmarc.policy === 'reject' && record.status !== 'verified'\n ? 'error'\n : 'info'\n }\n >\n {record.dmarc.consequence}\n </Alert>\n ) : null}\n\n {record.records?.length &&\n record.records.some((entry) => entry.value) ? (\n <>\n <Divider />\n <Typography variant=\"body2\" sx={{ fontWeight: 'bold' }}>\n {'Publish these records'}\n </Typography>\n <Stack spacing={2}>\n {record.records\n .filter((entry) => entry.value)\n .map((entry) => (\n <RecordRow\n key={`${entry.type}:${entry.name}`}\n record={entry}\n missing={missing.has(`${entry.type}:${entry.name}`)}\n />\n ))}\n {record.dmarcSuggestion &&\n record.dmarc?.policy === 'absent' ? (\n <RecordRow record={record.dmarcSuggestion} missing={false} />\n ) : null}\n </Stack>\n </>\n ) : null}\n\n <Divider />\n <Stack direction=\"row\" spacing={1} sx={{ flexWrap: 'wrap', gap: 1 }}>\n {record.status === 'requested' && canManage ? (\n <Button\n variant=\"contained\"\n disabled={busy}\n onClick={() => void handleRequestRecords()}\n >\n {busy ? 'Working…' : 'Request records'}\n </Button>\n ) : null}\n {record.status !== 'requested' ? (\n <Button\n variant=\"contained\"\n disabled={busy}\n onClick={() => void handleVerify()}\n >\n {busy ? 'Checking…' : 'Check DNS'}\n </Button>\n ) : null}\n {canManage && record.status === 'verified' && !isSelected ? (\n <Button\n variant=\"outlined\"\n startIcon={<MdiIcon path={mdiEmailCheckOutline.path} size={0.8} />}\n disabled={busy}\n onClick={() => void handleUse(domain)}\n >\n {'Send this site’s email as this domain'}\n </Button>\n ) : null}\n {canManage && isSelected ? (\n /*\n * Clearing the selection returns the site to the domain\n * PROVISIONED for it — `{label}.mail.aglyn.app` — not to the\n * shared pool, and the label says so. A site whose plan carries\n * no such domain is refused here with the reason, which is why\n * the control is worded as leaving this domain rather than\n * promising a destination it may not reach.\n */\n <Button\n variant=\"outlined\"\n disabled={busy}\n onClick={() => void handleUse('platform')}\n >\n {'Stop sending this site’s email as this domain'}\n </Button>\n ) : null}\n </Stack>\n </Stack>\n )}\n </CardDisplay>\n )\n\n return (\n <>\n {/* The page heading and the trail name the domain; this card is\n then free to say what it holds rather than repeating the title. */}\n <PageHeaderRecord title={domain} />\n {card}\n </>\n )\n}\nSendingDomainDetail.displayName = 'SendingDomainDetail'\n\nexport default SendingDomainDetail\n"],"names":["PageHeaderRecord","pluginDocsHelp","mdiDeleteOutline","mdiEmailCheckOutline","CardDisplay","MdiIcon","useConfirmationContext","RowActionsMenu","useSnackbar","Alert","Button","Chip","Divider","Stack","Typography","useRouter","useCallback","useEffect","useRef","useState","describeSendingDomain","describeSendingDomainRemoval","INCONCLUSIVE_CHECK","useSendingApi","RecordRow","props","record","missing","target","priority","value","spacing","direction","sx","alignItems","size","label","type","variant","fontWeight","purpose","required","color","component","p","bgcolor","borderRadius","fontFamily","whiteSpace","overflowWrap","name","note","displayName","SendingDomainDetail","hostId","domain","basePath","call","router","enqueueSnackbar","notifyRef","current","confirm","identity","setIdentity","setRecord","loading","setLoading","busy","setBusy","unreachable","setUnreachable","load","view","domains","path","method","query","response","ok","payload","error","orgId","String","found","find","one","handleVerify","body","action","status","verified","handleRequestRecords","handleUse","next","from","handleRelease","selected","platformDomain","confirmationButtonProps","then","catch","push","state","pendingProvider","issueError","lastIssueError","lastMissing","Set","canManage","isSelected","actions","key","icon","destructive","onClick","card","header","help","anchor","contentGutterX","contentGutterY","contentBordered","HeaderProps","length","items","severity","sending","text","dmarc","policy","consequence","records","some","entry","filter","map","has","dmarcSuggestion","flexWrap","gap","disabled","startIcon","title"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,SAASA,gBAAgB,EAAEC,cAAc,QAAQ,eAAc;AAC/D,SAASC,gBAAgB,EAAEC,oBAAoB,QAAQ,yBAAwB;AAC/E,SAASC,WAAW,EAAEC,OAAO,EAAEC,sBAAsB,QAAQ,uBAAsB;AACnF,OAAOC,oBAEA,6DAA4D;AACnE,SAASC,WAAW,QAAQ,8BAA6B;AACzD,SACEC,KAAK,EACLC,MAAM,EACNC,IAAI,EACJC,OAAO,EACPC,KAAK,EACLC,UAAU,QACL,gBAAe;AACtB,SAASC,SAAS,QAAQ,kBAAiB;AAC3C,SAASC,WAAW,EAAEC,SAAS,EAAEC,MAAM,EAAEC,QAAQ,QAAQ,QAAO;AAChE,SACEC,qBAAqB,EACrBC,4BAA4B,EAC5BC,kBAAkB,QACb,oCAAgC;AACvC,SACEC,aAAa,QAIR,gEAA+D;AAQtE,oEAAoE,GACpE,SAASC,UAAUC,KAAyD;IAC1E,MAAM,EAAEC,MAAM,EAAEC,OAAO,EAAE,GAAGF;IAC5B,MAAMG,SAASF,OAAOG,QAAQ,GAC1B,GAAGH,OAAOG,QAAQ,CAAC,CAAC,EAAEH,OAAOI,KAAK,EAAE,GACpCJ,OAAOI,KAAK;IAChB,qBACE,MAACjB;QAAMkB,SAAS;;0BACd,MAAClB;gBAAMmB,WAAU;gBAAMD,SAAS;gBAAGE,IAAI;oBAAEC,YAAY;gBAAS;;kCAC5D,KAACvB;wBAAKwB,MAAK;wBAAQC,OAAOV,OAAOW,IAAI;wBAAEC,SAAQ;;kCAC/C,KAACxB;wBAAWwB,SAAQ;wBAAQL,IAAI;4BAAEM,YAAY;wBAAO;kCAClDb,OAAOc,OAAO,KAAK,QAChB,kBACAd,OAAOc,OAAO,KAAK,SACjB,gBACAd,OAAOc,OAAO,KAAK,gBACjB,mBACA;;oBAETd,OAAOe,QAAQ,GAAG,qBACjB,KAAC9B;wBAAKwB,MAAK;wBAAQC,OAAM;wBAAWE,SAAQ;;oBAE7CX,wBAAU,KAAChB;wBAAKwB,MAAK;wBAAQC,OAAM;wBAAeM,OAAM;yBAAe;;;0BAE1E,KAAC5B;gBACCwB,SAAQ;gBACRK,WAAU;gBACVV,IAAI;oBACFW,GAAG;oBACHC,SAAS;oBACTC,cAAc;oBACdC,YAAY;oBACZC,YAAY;oBACZC,cAAc;gBAChB;0BAEC,GAAGvB,OAAOW,IAAI,CAAC,EAAE,EAAEX,OAAOwB,IAAI,CAAC,KAAK,EAAEtB,UAAU,oBAAoB;;0BAEvE,KAACd;gBAAWwB,SAAQ;gBAAQI,OAAM;0BAC/BhB,OAAOyB,IAAI;;;;AAIpB;AACA3B,UAAU4B,WAAW,GAAG;AAExB;;;;;;;;;;;;;;;CAeC,GACD,OAAO,SAASC,oBAAoB5B,KAA+B;;QAyRxDC,iBAkBKA;IA1Sd,MAAM,EAAE4B,MAAM,EAAEC,MAAM,EAAEC,QAAQ,EAAE,GAAG/B;IACrC,MAAMgC,OAAOlC;IACb,MAAMmC,SAAS3C;IACf,MAAM,EAAE4C,eAAe,EAAE,GAAGnD;IAC5B;;;;;;;GAOC,GACD,MAAMoD,YAAY1C,OAAOyC;IACzBC,UAAUC,OAAO,GAAGF;IACpB,MAAM,EAAEG,OAAO,EAAE,GAAGxD;IAEpB,MAAM,CAACyD,UAAUC,YAAY,GAAG7C,SAAqC;IACrE,MAAM,CAACO,QAAQuC,UAAU,GAAG9C,SAAmC;IAC/D,MAAM,CAAC+C,SAASC,WAAW,GAAGhD,SAAS;IACvC,MAAM,CAACiD,MAAMC,QAAQ,GAAGlD,SAAS;IACjC;;;;;GAKC,GACD,MAAM,CAACmD,aAAaC,eAAe,GAAGpD,SAAS;IAE/C,MAAMqD,OAAOxD,YAAY;;YAeFyD,eASNC;QAvBfP,WAAW;QACX,MAAMM,OAAO,MAAMhB,KAAK;YACtBkB,MAAM;YACNC,QAAQ;YACRC,OAAO;gBAAEvB;YAAO;QAClB;QACA,IAAI,CAACmB,KAAKK,QAAQ,CAACC,EAAE,EAAE;;gBAGnBN;YAFFN,WAAW;YACX,OAAO,KAAKP,UAAUC,OAAO,WAC3BY,iBAAAA,KAAKO,OAAO,qBAAZP,eAAcQ,KAAK,oBAAI,uCACvB;gBAAE3C,SAAS;YAAU;QAEzB;QACA0B,YAAYS,KAAKO,OAAO;QACxB,MAAME,QAAQC,gBAAOV,gBAAAA,KAAKO,OAAO,qBAAZP,cAAcS,KAAK,mBAAI;QAC5C,MAAMR,UAAUQ,QACZ,MAAMzB,KAAK;YACTkB,MAAM;YACNC,QAAQ;YACRC,OAAO;gBAAEK;YAAM;QACjB,KACA;QACJf,WAAW;QACX,MAAMiB,QAAQ,UAACV,4BAAAA,mBAAAA,QAASM,OAAO,qBAAhBN,iBAAkBA,OAAO,oBAAI,EAAE,EAAEW,IAAI,CAClD,CAACC,MAA2BA,IAAI/B,MAAM,KAAKA;QAE7CU,UAAUmB,gBAAAA,QAAS;IACrB,GAAG;QAAC3B;QAAMH;QAAQC;KAAO;IAEzBtC,UAAU;QACR,KAAKuD;IACP,GAAG;QAACA;KAAK;IAET,MAAMe,eAAevE,YAAY;QAC/B,IAAIoD,QAAQ,EAACL,4BAAAA,SAAUmB,KAAK,GAAE;QAC9Bb,QAAQ;QACRE,eAAe;QACf,MAAM,EAAEO,QAAQ,EAAEE,OAAO,EAAE,GAAG,MAAMvB,KAAK;YACvCkB,MAAM;YACNC,QAAQ;YACRY,MAAM;gBAAEN,OAAOnB,SAASmB,KAAK;gBAAE3B;gBAAQkC,QAAQ;YAAS;QAC1D;QACApB,QAAQ;QACR;;;;;;;;KAQC,GACD,IAAIS,SAASY,MAAM,KAAK,KAAK;YAC3BnB,eAAe;YACf;QACF;QACA,IAAI,CAACO,SAASC,EAAE,EAAE;;YAChB,OAAO,KAAKnB,UAAUC,OAAO,SAACmB,2BAAAA,QAASC,KAAK,mBAAI,2BAA2B;gBACzE3C,SAAS;YACX;QACF;QACA,MAAMkC;QACNZ,UAAUC,OAAO,CACfmB,CAAAA,2BAAAA,QAASW,QAAQ,IACb,oCACA,4CACJ;YAAErD,SAAS0C,CAAAA,2BAAAA,QAASW,QAAQ,IAAG,YAAY;QAAO;IAEtD,GAAG;QAACvB;QAAMX;QAAMM,4BAAAA,SAAUmB,KAAK;QAAE3B;QAAQiB;KAAK;IAE9C,MAAMoB,uBAAuB5E,YAAY;QACvC,IAAIoD,QAAQ,EAACL,4BAAAA,SAAUmB,KAAK,GAAE;QAC9Bb,QAAQ;QACR,MAAM,EAAES,QAAQ,EAAEE,OAAO,EAAE,GAAG,MAAMvB,KAAK;YACvCkB,MAAM;YACNC,QAAQ;YACRY,MAAM;gBAAEN,OAAOnB,SAASmB,KAAK;gBAAE3B;gBAAQkC,QAAQ;YAAU;QAC3D;QACApB,QAAQ;QACR,IAAI,CAACS,SAASC,EAAE,EAAE;;YAChB,OAAO,KAAKnB,UAAUC,OAAO,SAACmB,2BAAAA,QAASC,KAAK,mBAAI,6BAA6B;gBAC3E3C,SAAS;YACX;QACF;QACA,MAAMkC;IACR,GAAG;QAACJ;QAAMX;QAAMM,4BAAAA,SAAUmB,KAAK;QAAE3B;QAAQiB;KAAK;IAE9C,MAAMqB,YAAY7E,YAChB,OAAO8E;QACL,IAAI1B,MAAM;QACVC,QAAQ;QACR,MAAM,EAAES,QAAQ,EAAEE,OAAO,EAAE,GAAG,MAAMvB,KAAK;YACvCkB,MAAM;YACNC,QAAQ;YACRY,MAAM;gBAAElC;gBAAQC,QAAQuC;YAAK;QAC/B;QACAzB,QAAQ;QACR,IAAI,CAACS,SAASC,EAAE,EAAE;;YAChB,OAAO,KAAKnB,UAAUC,OAAO,SAACmB,2BAAAA,QAASC,KAAK,mBAAI,iCAAiC;gBAC/E3C,SAAS;YACX;QACF;QACA,MAAMkC;QACN;;;;;;;OAOC,GACDZ,UAAUC,OAAO,CACfmB,CAAAA,2BAAAA,QAASe,IAAI,IACT,CAAC,uBAAuB,EAAEf,QAAQe,IAAI,EAAE,GACxC,2CACJ;YAAEzD,SAAS;QAAU;IAEzB,GACA;QAAC8B;QAAMX;QAAMH;QAAQkB;KAAK;IAG5B,MAAMwB,gBAAgBhF,YAAY;QAChC,IAAI,EAAC+C,4BAAAA,SAAUmB,KAAK,GAAE;QACtB;;;;;;;;KAQC,GACD,MAAMH,KAAK,MAAMjB,QAAQ,aACpBzC,6BAA6B;YAC9BkC;YACA0C,UAAUlC,SAASkC,QAAQ;YAC3BC,gBAAgBnC,SAASmC,cAAc;QACzC;YACAC,yBAAyB;gBAAEzD,OAAO;YAAQ;WAE1C,iEAAiE;QACjE,4CAA4C;SAC3C0D,IAAI,CAAC,IAAM,MACXC,KAAK,CAAC,IAAM;QACf,IAAI,CAACtB,IAAI;QACT,MAAM,EAAED,QAAQ,EAAEE,OAAO,EAAE,GAAG,MAAMvB,KAAK;YACvCkB,MAAM;YACNC,QAAQ;YACRC,OAAO;gBAAEK,OAAOnB,SAASmB,KAAK;gBAAE3B;YAAO;QACzC;QACA,IAAI,CAACuB,SAASC,EAAE,EAAE;;YAChB,OAAO,KAAKnB,UAAUC,OAAO,SAACmB,2BAAAA,QAASC,KAAK,mBAAI,+BAA+B;gBAC7E3C,SAAS;YACX;QACF;QACAoB,OAAO4C,IAAI,CAAC,GAAG9C,SAAS,QAAQ,CAAC;IACnC,GAAG;QAACM;QAASL;QAAMM;QAAUR;QAAQG;QAAQF;KAAS;IAEtD,MAAM+C,QAAQnF,sBAAsB;QAClCsE,MAAM,UAAEhE,0BAAAA,OAAQgE,MAAM,mBAAI;QAC1Bc,eAAe,WAAE9E,0BAAAA,OAAQ8E,eAAe,oBAAI9E,CAAAA,0BAAAA,OAAQgE,MAAM,MAAK;QAC/De,UAAU,WAAE/E,0BAAAA,OAAQgF,cAAc,oBAAI;QACtC/E,OAAO,WAAED,0BAAAA,OAAQiF,WAAW,oBAAI;IAClC;IACA,MAAMhF,UAAU,IAAIiF,aAAIlF,0BAAAA,OAAQiF,WAAW,oBAAI,EAAE;IACjD,MAAME,YAAY9C,CAAAA,4BAAAA,SAAU8C,SAAS,MAAK;IAC1C,MAAMC,aAAa/C,CAAAA,4BAAAA,SAAUkC,QAAQ,MAAK1C;IAE1C,MAAMwD,UAAgCF,YAClC;QACE;YACEG,KAAK;YACL5E,OAAO;YACP6E,oBAAM,KAAC5G;gBAAQsE,MAAMzE,iBAAiByE,IAAI;gBAAExC,MAAM;;YAClD+E,aAAa;YACbC,SAAS,IAAM,KAAKnB;QACtB;KACD,GACD,EAAE;IAEN;yCACuC,GACvC,MAAMoB,qBACJ,KAAChH;QACCiH,QAAQ;QACRC,MAAMrH,eAAe,kBAAkB;YACrCsH,QAAQ;QACV;QACAC,cAAc;QACdC,cAAc;QACdC,iBAAgB;QAChBC,aAAa;YACXlC,QAAQsB,QAAQa,MAAM,iBAAG,KAACrH;gBAAesH,OAAOd;iBAAc;QAChE;kBAEC7C,wBACC,KAACpD;YAAWwB,SAAQ;YAAQI,OAAM;sBAC/B;aAED,CAAChB,uBACH,KAACjB;YAAMqH,UAAS;sBACb,GAAGvE,OAAO,oDAAoD,CAAC,GAC9D,CAAC,wDAAwD,CAAC;2BAG9D,MAAC1C;YAAMkB,SAAS;;8BACd,MAAClB;oBAAMmB,WAAU;oBAAMD,SAAS;oBAAGE,IAAI;wBAAEC,YAAY;oBAAS;;sCAC5D,KAACvB;4BACCyB,OAAOmE,MAAMnE,KAAK;4BAClBM,OAAO6D,MAAM7D,KAAK;4BAClBJ,SAASiE,MAAMwB,OAAO,GAAG,WAAW;;wBAErCjB,2BACC,KAACnG;4BAAKyB,OAAM;4BAA0BM,OAAM;4BAAUP,MAAK;6BACzD;;;8BAGN,KAAC1B;oBAAMqH,UAAUvB,MAAMuB,QAAQ;8BAAGvB,MAAMyB,IAAI;;gBAO3C1D,4BACC,MAAC7D;oBAAMqH,UAAUxG,mBAAmBwG,QAAQ;oBAAEb,MAAM;;sCAClD,KAACnG;4BAAWwB,SAAQ;4BAAQL,IAAI;gCAAEM,YAAY;4BAAO;sCAClDjB,mBAAmBc,KAAK;;sCAE3B,KAACtB;4BAAWwB,SAAQ;sCAAShB,mBAAmB0G,IAAI;;;qBAEpD;gBAQHtG,OAAOuG,KAAK,iBACX,KAACxH;oBACCqH,UACEpG,OAAOuG,KAAK,CAACC,MAAM,KAAK,YAAYxG,OAAOgE,MAAM,KAAK,aAClD,UACA;8BAGLhE,OAAOuG,KAAK,CAACE,WAAW;qBAEzB;gBAEHzG,EAAAA,kBAAAA,OAAO0G,OAAO,qBAAd1G,gBAAgBkG,MAAM,KACvBlG,OAAO0G,OAAO,CAACC,IAAI,CAAC,CAACC,QAAUA,MAAMxG,KAAK,kBACxC;;sCACE,KAAClB;sCACD,KAACE;4BAAWwB,SAAQ;4BAAQL,IAAI;gCAAEM,YAAY;4BAAO;sCAClD;;sCAEH,MAAC1B;4BAAMkB,SAAS;;gCACbL,OAAO0G,OAAO,CACZG,MAAM,CAAC,CAACD,QAAUA,MAAMxG,KAAK,EAC7B0G,GAAG,CAAC,CAACF,sBACJ,KAAC9G;wCAECE,QAAQ4G;wCACR3G,SAASA,QAAQ8G,GAAG,CAAC,GAAGH,MAAMjG,IAAI,CAAC,CAAC,EAAEiG,MAAMpF,IAAI,EAAE;uCAF7C,GAAGoF,MAAMjG,IAAI,CAAC,CAAC,EAAEiG,MAAMpF,IAAI,EAAE;gCAKvCxB,OAAOgH,eAAe,IACvBhH,EAAAA,gBAAAA,OAAOuG,KAAK,qBAAZvG,cAAcwG,MAAM,MAAK,yBACvB,KAAC1G;oCAAUE,QAAQA,OAAOgH,eAAe;oCAAE/G,SAAS;qCAClD;;;;qBAGN;8BAEJ,KAACf;8BACD,MAACC;oBAAMmB,WAAU;oBAAMD,SAAS;oBAAGE,IAAI;wBAAE0G,UAAU;wBAAQC,KAAK;oBAAE;;wBAC/DlH,OAAOgE,MAAM,KAAK,eAAemB,0BAChC,KAACnG;4BACC4B,SAAQ;4BACRuG,UAAUzE;4BACV+C,SAAS,IAAM,KAAKvB;sCAEnBxB,OAAO,aAAa;6BAErB;wBACH1C,OAAOgE,MAAM,KAAK,4BACjB,KAAChF;4BACC4B,SAAQ;4BACRuG,UAAUzE;4BACV+C,SAAS,IAAM,KAAK5B;sCAEnBnB,OAAO,cAAc;6BAEtB;wBACHyC,aAAanF,OAAOgE,MAAM,KAAK,cAAc,CAACoB,2BAC7C,KAACpG;4BACC4B,SAAQ;4BACRwG,yBAAW,KAACzI;gCAAQsE,MAAMxE,qBAAqBwE,IAAI;gCAAExC,MAAM;;4BAC3D0G,UAAUzE;4BACV+C,SAAS,IAAM,KAAKtB,UAAUtC;sCAE7B;6BAED;wBACHsD,aAAaC,aACZ;;;;;;;eAOC,iBACD,KAACpG;4BACC4B,SAAQ;4BACRuG,UAAUzE;4BACV+C,SAAS,IAAM,KAAKtB,UAAU;sCAE7B;6BAED;;;;;;IAOd,qBACE;;0BAGE,KAAC7F;gBAAiB+I,OAAOxF;;YACxB6D;;;AAGP;AACA/D,oBAAoBD,WAAW,GAAG;AAElC,eAAeC,oBAAmB"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface SendingDomainsCardProps {
|
|
2
|
+
hostId: string;
|
|
3
|
+
/** `/…/emails`, which every card in this section is handed. */
|
|
4
|
+
basePath: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* WHERE A WORKSPACE PROVES IT OWNS THE DOMAIN ITS MAIL LEAVES ON.
|
|
8
|
+
*
|
|
9
|
+
* ## The two halves on one screen, and why they are not one control
|
|
10
|
+
*
|
|
11
|
+
* A domain is proved by the ORG — publishing a DKIM record is a statement
|
|
12
|
+
* about a zone, and an agency running four sites on `client.com` should make
|
|
13
|
+
* it once. Which of the proved domains THIS SITE sends as is a separate,
|
|
14
|
+
* per-site choice, because the `From:` a recipient sees belongs to the site.
|
|
15
|
+
*
|
|
16
|
+
* So the top of this card says what this site sends as right now, and the
|
|
17
|
+
* table below is the org's set of domains. Collapsing them would make one of
|
|
18
|
+
* the two questions unanswerable: with one control per site, four sites would
|
|
19
|
+
* publish the same record four times; with one control per org, an agency
|
|
20
|
+
* could not give two clients two different `From:` lines.
|
|
21
|
+
*
|
|
22
|
+
* ## Every state gets its own sentence
|
|
23
|
+
*
|
|
24
|
+
* Five situations, and `describeSendingDomain` holds four of them — the fifth,
|
|
25
|
+
* a check nobody answered, is deliberately not a state at all. It rides the
|
|
26
|
+
* surface as a transient notice beside whatever the record still says, which
|
|
27
|
+
* is the whole reason the model file exists.
|
|
28
|
+
*/
|
|
29
|
+
export declare function SendingDomainsCard(props: SendingDomainsCardProps): import("react").JSX.Element;
|
|
30
|
+
export declare namespace SendingDomainsCard {
|
|
31
|
+
var displayName: string;
|
|
32
|
+
}
|
|
33
|
+
export default SendingDomainsCard;
|