@promptbook/cli 0.114.0-10 → 0.114.0-12
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/apps/agents-server/src/app/admin/messages/MessagesClient.tsx +8 -1
- package/apps/agents-server/src/app/admin/messages/send-email/SendEmailClient.tsx +193 -63
- package/apps/agents-server/src/app/admin/messages/send-email/actions.ts +25 -28
- package/apps/agents-server/src/app/admin/messages/send-email/emailTesting.ts +153 -0
- package/apps/agents-server/src/app/admin/messages/send-email/emailTestingAccess.ts +30 -0
- package/apps/agents-server/src/app/admin/messages/send-email/page.tsx +18 -5
- package/apps/agents-server/src/app/api/internal/agent-goal-chat-planned-messages/route.ts +34 -7
- package/apps/agents-server/src/message-providers/email/stalwart/StalwartMessageProvider.ts +2 -1
- package/apps/agents-server/src/utils/email/agentEmailAddress.ts +14 -0
- package/apps/agents-server/src/utils/email/sendEmailThroughStalwart.ts +2 -1
- package/esm/index.es.js +404 -115
- package/esm/index.es.js.map +1 -1
- package/esm/scripts/run-codex-prompts/common/runGoScript/$spawnLoggedBashScript.d.ts +25 -0
- package/esm/scripts/run-codex-prompts/common/runGoScript/$terminateLoggedBashProcessTree.d.ts +11 -0
- package/esm/scripts/run-codex-prompts/common/runGoScript/scriptExecutionLog.d.ts +4 -0
- package/esm/src/book-2.0/agent-source/AgentSourceParseResult.d.ts +5 -0
- package/esm/src/book-2.0/agent-source/ParsedUnknownCommitment.d.ts +26 -0
- package/esm/src/book-2.0/agent-source/createAgentModelRequirementsWithCommitments.unknown.test.d.ts +1 -0
- package/esm/src/book-2.0/agent-source/getBookCommitmentLineType.d.ts +19 -0
- package/esm/src/book-2.0/agent-source/parseAgentSourceWithCommitments.unknown.test.d.ts +1 -0
- package/esm/src/book-components/BookEditor/createUnknownCommitmentDiagnostics.d.ts +28 -0
- package/esm/src/book-components/BookEditor/createUnknownCommitmentDiagnostics.test.d.ts +1 -0
- package/esm/src/cli/cli-commands/common/harness/$checkHarnessInstallation.d.ts +3 -3
- package/esm/src/cli/cli-commands/common/harness/$checkHarnessInstallation.test.d.ts +1 -0
- package/esm/src/cli/cli-commands/common/harness/$ensureHarnessInstallations.d.ts +3 -3
- package/esm/src/cli/cli-commands/common/harness/HarnessInstallationStatus.d.ts +3 -2
- package/esm/src/cli/cli-commands/common/harnessUpdateCliOptions.d.ts +29 -0
- package/esm/src/cli/cli-commands/common/harnessUpdateCliOptions.test.d.ts +1 -0
- package/esm/src/executables/platforms/locateAppOnWindows.d.ts +1 -1
- package/esm/src/executables/platforms/locateAppOnWindows.test.d.ts +1 -0
- package/esm/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/book-2.0/agent-source/AgentSourceParseResult.ts +6 -0
- package/src/book-2.0/agent-source/ParsedUnknownCommitment.ts +29 -0
- package/src/book-2.0/agent-source/createAgentModelRequirementsWithCommitments/augmentAgentModelRequirementsFromSource.ts +38 -1
- package/src/book-2.0/agent-source/getBookCommitmentLineType.ts +36 -0
- package/src/book-2.0/agent-source/parseAgentSourceWithCommitments.ts +128 -61
- package/src/book-components/BookEditor/BookEditorMonaco.tsx +6 -1
- package/src/book-components/BookEditor/BookEditorMonacoTokenization.ts +3 -1
- package/src/book-components/BookEditor/createUnknownCommitmentDiagnostics.ts +50 -0
- package/src/cli/cli-commands/coder/boilerplateTemplates.ts +3 -15
- package/src/cli/cli-commands/coder/init.ts +11 -2
- package/src/cli/cli-commands/coder/ping.ts +11 -1
- package/src/cli/cli-commands/coder/run.ts +11 -2
- package/src/cli/cli-commands/coder/server.ts +11 -2
- package/src/cli/cli-commands/common/harness/$applyHarnessInstallationStatus.ts +5 -0
- package/src/cli/cli-commands/common/harness/$checkHarnessInstallation.ts +17 -5
- package/src/cli/cli-commands/common/harness/$ensureHarnessInstallations.ts +6 -3
- package/src/cli/cli-commands/common/harness/HarnessInstallationStatus.ts +3 -2
- package/src/cli/cli-commands/common/harnessUpdateCliOptions.ts +49 -0
- package/src/executables/platforms/locateAppOnWindows.ts +12 -9
- package/src/other/templates/getTemplatesPipelineCollection.ts +881 -685
- package/src/version.ts +2 -2
- package/src/versions.txt +1 -0
- package/umd/index.umd.js +403 -114
- package/umd/index.umd.js.map +1 -1
- package/umd/scripts/run-codex-prompts/common/runGoScript/$spawnLoggedBashScript.d.ts +25 -0
- package/umd/scripts/run-codex-prompts/common/runGoScript/$terminateLoggedBashProcessTree.d.ts +11 -0
- package/umd/scripts/run-codex-prompts/common/runGoScript/scriptExecutionLog.d.ts +4 -0
- package/umd/src/book-2.0/agent-source/AgentSourceParseResult.d.ts +5 -0
- package/umd/src/book-2.0/agent-source/ParsedUnknownCommitment.d.ts +26 -0
- package/umd/src/book-2.0/agent-source/createAgentModelRequirementsWithCommitments.unknown.test.d.ts +1 -0
- package/umd/src/book-2.0/agent-source/getBookCommitmentLineType.d.ts +19 -0
- package/umd/src/book-2.0/agent-source/parseAgentSourceWithCommitments.unknown.test.d.ts +1 -0
- package/umd/src/book-components/BookEditor/createUnknownCommitmentDiagnostics.d.ts +28 -0
- package/umd/src/book-components/BookEditor/createUnknownCommitmentDiagnostics.test.d.ts +1 -0
- package/umd/src/cli/cli-commands/common/harness/$checkHarnessInstallation.d.ts +3 -3
- package/umd/src/cli/cli-commands/common/harness/$checkHarnessInstallation.test.d.ts +1 -0
- package/umd/src/cli/cli-commands/common/harness/$ensureHarnessInstallations.d.ts +3 -3
- package/umd/src/cli/cli-commands/common/harness/HarnessInstallationStatus.d.ts +3 -2
- package/umd/src/cli/cli-commands/common/harnessUpdateCliOptions.d.ts +29 -0
- package/umd/src/cli/cli-commands/common/harnessUpdateCliOptions.test.d.ts +1 -0
- package/umd/src/executables/platforms/locateAppOnWindows.d.ts +1 -1
- package/umd/src/executables/platforms/locateAppOnWindows.test.d.ts +1 -0
- package/umd/src/version.d.ts +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import Link from 'next/link';
|
|
3
4
|
import { useEffect, useMemo, useState } from 'react';
|
|
4
5
|
import { useServerLanguage } from '../../../components/ServerLanguage/ServerLanguageProvider';
|
|
5
6
|
import { Card } from '../../../components/Homepage/Card';
|
|
@@ -175,7 +176,13 @@ export function MessagesClient() {
|
|
|
175
176
|
Inspect all inbound and outbound messages and their statuses.
|
|
176
177
|
</p>
|
|
177
178
|
</div>
|
|
178
|
-
<div className="flex items-end gap-4 text-sm text-gray-500 md:text-right">
|
|
179
|
+
<div className="flex flex-wrap items-end gap-4 text-sm text-gray-500 md:text-right">
|
|
180
|
+
<Link
|
|
181
|
+
href="/admin/messages/send-email"
|
|
182
|
+
className="rounded-md border border-blue-600 px-3 py-2 text-sm font-medium text-blue-700 hover:bg-blue-50"
|
|
183
|
+
>
|
|
184
|
+
Send test email
|
|
185
|
+
</Link>
|
|
179
186
|
<div>
|
|
180
187
|
<div className="text-xl font-semibold text-gray-900">{total.toLocaleString()}</div>
|
|
181
188
|
<div className="text-xs uppercase tracking-wide text-gray-400">Total messages</div>
|
|
@@ -1,17 +1,131 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState } from 'react';
|
|
3
|
+
import { type FormEvent, useState } from 'react';
|
|
4
4
|
import { assertsError } from '../../../../../../../src/errors/assertsError';
|
|
5
5
|
import { sendEmailAction } from './actions';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* Existing agent address offered as a one-click form prefill.
|
|
9
9
|
*/
|
|
10
|
-
export
|
|
10
|
+
export type EmailTestingAgentOption = {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
readonly label: string;
|
|
13
|
+
readonly address: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Props accepted by the email testing client.
|
|
18
|
+
*/
|
|
19
|
+
type SendEmailClientProps = {
|
|
20
|
+
readonly currentServerDomain: string;
|
|
21
|
+
readonly isGlobalAdmin: boolean;
|
|
22
|
+
readonly agentEmailOptions: ReadonlyArray<EmailTestingAgentOption>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Props accepted by an address editor with optional agent-address prefills.
|
|
27
|
+
*/
|
|
28
|
+
type EmailTestingAddressInputProps = {
|
|
29
|
+
readonly label: string;
|
|
30
|
+
readonly name: 'from' | 'to';
|
|
31
|
+
readonly value: string;
|
|
32
|
+
readonly placeholder: string;
|
|
33
|
+
readonly helperText: string;
|
|
34
|
+
readonly agentEmailOptions: ReadonlyArray<EmailTestingAgentOption>;
|
|
35
|
+
readonly isDisabled: boolean;
|
|
36
|
+
readonly onValueChange: (value: string) => void;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Renders one arbitrary email-address input and lets administrators pre-fill it from an existing agent.
|
|
41
|
+
*/
|
|
42
|
+
function EmailTestingAddressInput({
|
|
43
|
+
label,
|
|
44
|
+
name,
|
|
45
|
+
value,
|
|
46
|
+
placeholder,
|
|
47
|
+
helperText,
|
|
48
|
+
agentEmailOptions,
|
|
49
|
+
isDisabled,
|
|
50
|
+
onValueChange,
|
|
51
|
+
}: EmailTestingAddressInputProps) {
|
|
52
|
+
const helperTextId = `${name}-helper-text`;
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div>
|
|
56
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
57
|
+
<label htmlFor={name} className="block text-sm font-medium text-gray-700">
|
|
58
|
+
{label}
|
|
59
|
+
</label>
|
|
60
|
+
{agentEmailOptions.length > 0 ? (
|
|
61
|
+
<select
|
|
62
|
+
aria-label={`Pre-fill ${label.toLowerCase()} with an existing agent address`}
|
|
63
|
+
value=""
|
|
64
|
+
onChange={(event) => {
|
|
65
|
+
if (event.target.value) {
|
|
66
|
+
onValueChange(event.target.value);
|
|
67
|
+
}
|
|
68
|
+
}}
|
|
69
|
+
disabled={isDisabled}
|
|
70
|
+
className="rounded-md border border-gray-300 bg-white px-2 py-1 text-xs text-gray-700"
|
|
71
|
+
>
|
|
72
|
+
<option value="">Pre-fill from an agent</option>
|
|
73
|
+
{agentEmailOptions.map((agent) => (
|
|
74
|
+
<option key={agent.id} value={agent.address}>
|
|
75
|
+
{agent.label} — {agent.address}
|
|
76
|
+
</option>
|
|
77
|
+
))}
|
|
78
|
+
</select>
|
|
79
|
+
) : null}
|
|
80
|
+
</div>
|
|
81
|
+
<input
|
|
82
|
+
id={name}
|
|
83
|
+
name={name}
|
|
84
|
+
type="text"
|
|
85
|
+
value={value}
|
|
86
|
+
onChange={(event) => onValueChange(event.target.value)}
|
|
87
|
+
placeholder={placeholder}
|
|
88
|
+
aria-describedby={helperTextId}
|
|
89
|
+
required
|
|
90
|
+
disabled={isDisabled}
|
|
91
|
+
className="mt-1 block w-full rounded-md border border-gray-300 p-2 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 disabled:bg-gray-100"
|
|
92
|
+
/>
|
|
93
|
+
<p id={helperTextId} className="mt-1 text-xs text-gray-500">
|
|
94
|
+
{helperText}
|
|
95
|
+
</p>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Explains the domain scope available to the current administrator.
|
|
102
|
+
*/
|
|
103
|
+
function getEmailTestingScopeMessage(currentServerDomain: string, isGlobalAdmin: boolean): string {
|
|
104
|
+
if (isGlobalAdmin) {
|
|
105
|
+
return 'As the superadmin, you can use a sender address from any domain.';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return `You can use any recipient address, but the sender must use ${currentServerDomain}.`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Renders and submits the administration email testing form.
|
|
113
|
+
*/
|
|
114
|
+
export function SendEmailClient({
|
|
115
|
+
currentServerDomain,
|
|
116
|
+
isGlobalAdmin,
|
|
117
|
+
agentEmailOptions,
|
|
118
|
+
}: SendEmailClientProps) {
|
|
11
119
|
const [status, setStatus] = useState<'IDLE' | 'LOADING' | 'SUCCESS' | 'ERROR'>('IDLE');
|
|
12
120
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
121
|
+
const [from, setFrom] = useState('');
|
|
122
|
+
const [to, setTo] = useState('');
|
|
123
|
+
const isLoading = status === 'LOADING';
|
|
13
124
|
|
|
14
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Sends the currently composed test email and displays the server-side outcome.
|
|
127
|
+
*/
|
|
128
|
+
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
|
15
129
|
event.preventDefault();
|
|
16
130
|
setStatus('LOADING');
|
|
17
131
|
setErrorMessage(null);
|
|
@@ -31,77 +145,93 @@ export function SendEmailClient() {
|
|
|
31
145
|
|
|
32
146
|
return (
|
|
33
147
|
<div className="container mx-auto p-4">
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
148
|
+
<div className="mx-auto mt-20 max-w-2xl space-y-6">
|
|
149
|
+
<div>
|
|
150
|
+
<h1 className="text-3xl font-light text-gray-900">Email testing</h1>
|
|
151
|
+
<p className="mt-1 text-sm text-gray-500">
|
|
152
|
+
Send a manually composed email through this server's email service.
|
|
153
|
+
</p>
|
|
154
|
+
<p className="mt-2 rounded-md bg-blue-50 px-3 py-2 text-sm text-blue-950">
|
|
155
|
+
{getEmailTestingScopeMessage(currentServerDomain, isGlobalAdmin)}
|
|
156
|
+
</p>
|
|
39
157
|
</div>
|
|
40
|
-
)}
|
|
41
158
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
159
|
+
{status === 'SUCCESS' && (
|
|
160
|
+
<div className="rounded border border-green-400 bg-green-100 px-4 py-3 text-green-700">
|
|
161
|
+
Email sent successfully!
|
|
162
|
+
</div>
|
|
163
|
+
)}
|
|
47
164
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
165
|
+
{status === 'ERROR' && (
|
|
166
|
+
<div className="rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700">
|
|
167
|
+
Error: {errorMessage}
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
|
|
171
|
+
<form onSubmit={handleSubmit} className="space-y-4 rounded-xl border border-gray-200 bg-white p-5 shadow-sm">
|
|
172
|
+
<EmailTestingAddressInput
|
|
173
|
+
label="From"
|
|
52
174
|
name="from"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
175
|
+
value={from}
|
|
176
|
+
onValueChange={setFrom}
|
|
177
|
+
placeholder={`Sender <sender@${currentServerDomain}>`}
|
|
178
|
+
helperText="Enter any valid sender address, or pre-fill one of this server's agents."
|
|
179
|
+
agentEmailOptions={agentEmailOptions}
|
|
180
|
+
isDisabled={isLoading}
|
|
57
181
|
/>
|
|
58
|
-
</div>
|
|
59
182
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
<input
|
|
183
|
+
<EmailTestingAddressInput
|
|
184
|
+
label="To"
|
|
63
185
|
name="to"
|
|
64
|
-
|
|
186
|
+
value={to}
|
|
187
|
+
onValueChange={setTo}
|
|
65
188
|
placeholder="recipient@example.com"
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
189
|
+
helperText="Separate multiple addresses with commas."
|
|
190
|
+
agentEmailOptions={agentEmailOptions}
|
|
191
|
+
isDisabled={isLoading}
|
|
69
192
|
/>
|
|
70
|
-
<p className="text-xs text-gray-500 mt-1">Separate multiple addresses with commas</p>
|
|
71
|
-
</div>
|
|
72
193
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
194
|
+
<div>
|
|
195
|
+
<label htmlFor="subject" className="block text-sm font-medium text-gray-700">
|
|
196
|
+
Subject
|
|
197
|
+
</label>
|
|
198
|
+
<input
|
|
199
|
+
id="subject"
|
|
200
|
+
name="subject"
|
|
201
|
+
type="text"
|
|
202
|
+
placeholder="Test email"
|
|
203
|
+
defaultValue="Test email"
|
|
204
|
+
required
|
|
205
|
+
disabled={isLoading}
|
|
206
|
+
className="mt-1 block w-full rounded-md border border-gray-300 p-2 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 disabled:bg-gray-100"
|
|
207
|
+
/>
|
|
208
|
+
</div>
|
|
84
209
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
210
|
+
<div>
|
|
211
|
+
<label htmlFor="body" className="block text-sm font-medium text-gray-700">
|
|
212
|
+
Body
|
|
213
|
+
</label>
|
|
214
|
+
<textarea
|
|
215
|
+
id="body"
|
|
216
|
+
name="body"
|
|
217
|
+
rows={6}
|
|
218
|
+
placeholder="Hello, this is a test email."
|
|
219
|
+
defaultValue="Hello, this is a test email."
|
|
220
|
+
required
|
|
221
|
+
disabled={isLoading}
|
|
222
|
+
className="mt-1 block w-full rounded-md border border-gray-300 p-2 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 disabled:bg-gray-100"
|
|
223
|
+
/>
|
|
224
|
+
</div>
|
|
96
225
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
226
|
+
<button
|
|
227
|
+
type="submit"
|
|
228
|
+
disabled={isLoading}
|
|
229
|
+
className="rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700 disabled:opacity-50"
|
|
230
|
+
>
|
|
231
|
+
{isLoading ? 'Sending...' : 'Send email'}
|
|
232
|
+
</button>
|
|
233
|
+
</form>
|
|
234
|
+
</div>
|
|
105
235
|
</div>
|
|
106
236
|
);
|
|
107
237
|
}
|
|
@@ -1,38 +1,35 @@
|
|
|
1
1
|
'use server';
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
3
|
+
import { NotAllowed } from '../../../../../../../src/errors/NotAllowed';
|
|
4
|
+
import { spaceTrim } from '../../../../../../../src/utils/organization/spaceTrim';
|
|
5
|
+
import { sendEmailThroughStalwart } from '../../../../utils/email/sendEmailThroughStalwart';
|
|
6
|
+
import {
|
|
7
|
+
assertEmailTestingSenderDomainAllowed,
|
|
8
|
+
createEmailTestingOutboundEmail,
|
|
9
|
+
readEmailTestingFormValues,
|
|
10
|
+
} from './emailTesting';
|
|
11
|
+
import { getEmailTestingAccessContext } from './emailTestingAccess';
|
|
8
12
|
|
|
9
13
|
/**
|
|
10
|
-
*
|
|
14
|
+
* Sends an administrator-composed test email through the bundled Stalwart email service.
|
|
11
15
|
*/
|
|
12
|
-
export async function sendEmailAction(formData: FormData) {
|
|
13
|
-
const
|
|
14
|
-
const to = formData.get('to') as string;
|
|
15
|
-
const subject = formData.get('subject') as string;
|
|
16
|
-
const body = formData.get('body') as string;
|
|
16
|
+
export async function sendEmailAction(formData: FormData): Promise<void> {
|
|
17
|
+
const accessContext = await getEmailTestingAccessContext();
|
|
17
18
|
|
|
18
|
-
if (!
|
|
19
|
-
throw new
|
|
19
|
+
if (!accessContext) {
|
|
20
|
+
throw new NotAllowed(
|
|
21
|
+
spaceTrim(`
|
|
22
|
+
You are not allowed to send test emails.
|
|
23
|
+
`),
|
|
24
|
+
);
|
|
20
25
|
}
|
|
21
26
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
direction: 'OUTBOUND',
|
|
28
|
-
sender,
|
|
29
|
-
recipients,
|
|
30
|
-
subject,
|
|
31
|
-
content: body as string_html,
|
|
32
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
33
|
-
threadId: crypto.randomUUID() as any,
|
|
34
|
-
cc: [],
|
|
35
|
-
attachments: [],
|
|
36
|
-
metadata: {},
|
|
27
|
+
const email = createEmailTestingOutboundEmail(readEmailTestingFormValues(formData));
|
|
28
|
+
assertEmailTestingSenderDomainAllowed({
|
|
29
|
+
sender: email.sender,
|
|
30
|
+
currentServerDomain: accessContext.currentServerDomain,
|
|
31
|
+
isGlobalAdmin: accessContext.isGlobalAdmin,
|
|
37
32
|
});
|
|
33
|
+
|
|
34
|
+
await sendEmailThroughStalwart(email);
|
|
38
35
|
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { string_email, string_emails, string_markdown } from '@promptbook-local/types';
|
|
2
|
+
import { NotAllowed } from '../../../../../../../src/errors/NotAllowed';
|
|
3
|
+
import { ParseError } from '../../../../../../../src/errors/ParseError';
|
|
4
|
+
import { spaceTrim } from '../../../../../../../src/utils/organization/spaceTrim';
|
|
5
|
+
import type { OutboundEmail } from '../../../../message-providers/email/_common/Email';
|
|
6
|
+
import { parseEmailAddress } from '../../../../message-providers/email/_common/utils/parseEmailAddress';
|
|
7
|
+
import { parseEmailAddresses } from '../../../../message-providers/email/_common/utils/parseEmailAddresses';
|
|
8
|
+
import { stringifyEmailAddress } from '../../../../message-providers/email/_common/utils/stringifyEmailAddress';
|
|
9
|
+
import { getEmailAddressDomain, normalizeEmailDomain } from '../../../../utils/email/agentEmailAddress';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Raw values submitted through the administration email testing form.
|
|
13
|
+
*/
|
|
14
|
+
export type EmailTestingFormValues = {
|
|
15
|
+
readonly from: string;
|
|
16
|
+
readonly to: string;
|
|
17
|
+
readonly subject: string;
|
|
18
|
+
readonly body: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Reads and validates the required values of the administration email testing form.
|
|
23
|
+
*/
|
|
24
|
+
export function readEmailTestingFormValues(formData: FormData): EmailTestingFormValues {
|
|
25
|
+
return {
|
|
26
|
+
from: readRequiredEmailTestingFormText(formData, 'from'),
|
|
27
|
+
to: readRequiredEmailTestingFormText(formData, 'to'),
|
|
28
|
+
subject: readRequiredEmailTestingFormText(formData, 'subject'),
|
|
29
|
+
body: readRequiredEmailTestingFormText(formData, 'body'),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Creates the normalized outbound email persisted and delivered by the testing tool.
|
|
35
|
+
*/
|
|
36
|
+
export function createEmailTestingOutboundEmail(formValues: EmailTestingFormValues): OutboundEmail {
|
|
37
|
+
const sender = parseEmailTestingSender(formValues.from);
|
|
38
|
+
const recipients = parseEmailTestingRecipients(formValues.to);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
channel: 'EMAIL',
|
|
42
|
+
direction: 'OUTBOUND',
|
|
43
|
+
sender,
|
|
44
|
+
recipients,
|
|
45
|
+
subject: formValues.subject,
|
|
46
|
+
content: formValues.body as string_markdown,
|
|
47
|
+
threadId: crypto.randomUUID(),
|
|
48
|
+
cc: [],
|
|
49
|
+
attachments: [],
|
|
50
|
+
metadata: {},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Ensures that a normal administrator sends test mail only from the current server's domain.
|
|
56
|
+
*/
|
|
57
|
+
export function assertEmailTestingSenderDomainAllowed(options: {
|
|
58
|
+
readonly sender: string_email;
|
|
59
|
+
readonly currentServerDomain: string;
|
|
60
|
+
readonly isGlobalAdmin: boolean;
|
|
61
|
+
}): void {
|
|
62
|
+
if (options.isGlobalAdmin) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const senderDomain = getEmailAddressDomain(options.sender);
|
|
67
|
+
const currentServerDomain = normalizeEmailDomain(options.currentServerDomain);
|
|
68
|
+
|
|
69
|
+
if (senderDomain === currentServerDomain) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
throw new NotAllowed(
|
|
74
|
+
spaceTrim(`
|
|
75
|
+
You are only allowed to send test emails from the current server's domain.
|
|
76
|
+
|
|
77
|
+
**Current server domain:** \`${currentServerDomain}\`
|
|
78
|
+
**Sender domain:** \`${senderDomain}\`
|
|
79
|
+
`),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Reads one required string value from the submitted email testing form.
|
|
85
|
+
*/
|
|
86
|
+
function readRequiredEmailTestingFormText(formData: FormData, fieldName: string): string {
|
|
87
|
+
const value = formData.get(fieldName);
|
|
88
|
+
|
|
89
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
90
|
+
throw new ParseError(
|
|
91
|
+
spaceTrim(`
|
|
92
|
+
The \`${fieldName}\` field is required to send a test email.
|
|
93
|
+
`),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Parses one sender while converting parser failures into a user-facing branded error.
|
|
102
|
+
*/
|
|
103
|
+
function parseEmailTestingSender(value: string): string_email {
|
|
104
|
+
try {
|
|
105
|
+
return stringifyEmailAddress(parseEmailAddress(value.trim() as string_email));
|
|
106
|
+
} catch (error) {
|
|
107
|
+
throw new ParseError(
|
|
108
|
+
spaceTrim(`
|
|
109
|
+
The \`from\` email address is invalid.
|
|
110
|
+
|
|
111
|
+
**Cause:** \`${getEmailTestingErrorMessage(error)}\`
|
|
112
|
+
`),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Parses one or more comma-separated recipients into transport-ready addresses.
|
|
119
|
+
*/
|
|
120
|
+
function parseEmailTestingRecipients(value: string): string_email[] {
|
|
121
|
+
let recipients: string_email[];
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
recipients = parseEmailAddresses(value.trim() as string_emails).map(stringifyEmailAddress);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
throw new ParseError(
|
|
127
|
+
spaceTrim(`
|
|
128
|
+
The \`to\` email address is invalid.
|
|
129
|
+
|
|
130
|
+
**Cause:** \`${getEmailTestingErrorMessage(error)}\`
|
|
131
|
+
`),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (recipients.length > 0) {
|
|
136
|
+
return recipients;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
throw new ParseError(
|
|
140
|
+
spaceTrim(`
|
|
141
|
+
The \`to\` email address is invalid.
|
|
142
|
+
|
|
143
|
+
**Cause:** \`No recipient email addresses were provided.\`
|
|
144
|
+
`),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Gets a safe textual reason from an arbitrary parsing failure.
|
|
150
|
+
*/
|
|
151
|
+
function getEmailTestingErrorMessage(error: unknown): string {
|
|
152
|
+
return error instanceof Error ? error.message : String(error);
|
|
153
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { $provideServer } from '../../../../tools/$provideServer';
|
|
2
|
+
import { isUserAdmin } from '../../../../utils/isUserAdmin';
|
|
3
|
+
import { isUserGlobalAdmin } from '../../../../utils/isUserGlobalAdmin';
|
|
4
|
+
import { normalizeEmailDomain } from '../../../../utils/email/agentEmailAddress';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Authorization and server scope available to the administration email testing tool.
|
|
8
|
+
*/
|
|
9
|
+
export type EmailTestingAccessContext = {
|
|
10
|
+
readonly currentServerDomain: string;
|
|
11
|
+
readonly isGlobalAdmin: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolves access to the email testing tool for either a current-server admin or the VPS superadmin.
|
|
16
|
+
*/
|
|
17
|
+
export async function getEmailTestingAccessContext(): Promise<EmailTestingAccessContext | null> {
|
|
18
|
+
const [isAdmin, isGlobalAdmin] = await Promise.all([isUserAdmin(), isUserGlobalAdmin()]);
|
|
19
|
+
|
|
20
|
+
if (!isAdmin && !isGlobalAdmin) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const server = await $provideServer();
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
currentServerDomain: normalizeEmailDomain(server.publicUrl.hostname),
|
|
28
|
+
isGlobalAdmin,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
import { ForbiddenPage } from '../../../../components/ForbiddenPage/ForbiddenPage';
|
|
2
|
-
import {
|
|
2
|
+
import { listAgentEmailIdentities } from '../../../../utils/email/listAgentEmailIdentities';
|
|
3
3
|
import { SendEmailClient } from './SendEmailClient';
|
|
4
|
+
import { getEmailTestingAccessContext } from './emailTestingAccess';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
+
* Renders the current server's email testing tool.
|
|
7
8
|
*/
|
|
8
9
|
export default async function AdminSendEmailPage() {
|
|
9
|
-
const
|
|
10
|
+
const accessContext = await getEmailTestingAccessContext();
|
|
10
11
|
|
|
11
|
-
if (!
|
|
12
|
+
if (!accessContext) {
|
|
12
13
|
return <ForbiddenPage />;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
const agentEmailOptions = (await listAgentEmailIdentities(accessContext.currentServerDomain)).map((agent) => ({
|
|
17
|
+
id: agent.permanentId,
|
|
18
|
+
label: agent.displayName,
|
|
19
|
+
address: agent.preferredEmail,
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<SendEmailClient
|
|
24
|
+
currentServerDomain={accessContext.currentServerDomain}
|
|
25
|
+
isGlobalAdmin={accessContext.isGlobalAdmin}
|
|
26
|
+
agentEmailOptions={agentEmailOptions}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
16
29
|
}
|