Skip to content

Commit 7d5d649

Browse files
committed
fix(plugin-email): Send test email refusals name the admin's next step, not a REST body
The three mail/test recipient refusals told the admin to pass "to" in the request body; the mail settings page has no recipient field. Each now says nothing was sent and what to do on the page. The `to` override is documented in resolveMailTestRecipient's TSDoc for API callers. Pins added per refusal. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016g7nYP9wdNx1GDa5XDr7Uy
1 parent d0a1361 commit 7d5d649

3 files changed

Lines changed: 75 additions & 5 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/plugin-email': patch
3+
---
4+
5+
fix(plugin-email): the Send test email refusals name what the admin does on the page instead of a REST recipe (#25424)
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
When **Send test email** (`mail/test`) could not go to the signed-in admin's own address, its refusal said to pass "to" in the request body. The mail settings page has no recipient field, so that advice could not be followed from it. Each refusal now says that nothing was sent and what the admin can do on the page: press Send test email signed in as an admin whose account has an email address, or press it again once the account can be read. A `to` in the request still wins, as before. It is now documented only in the handler's TSDoc, for API callers.
11+
12+
No route, response shape, status or code changes: only the message text does. A caller that compared these strings verbatim must update its copy.

‎packages/plugins/plugin-email/src/email-plugin.ts‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,15 @@ const ACCOUNT_OBJECT = 'sys_user';
180180
* reaches the caller in the result message, and it is their own address, so
181181
* nothing the caller could not already read is disclosed.
182182
*
183-
* Every refusal says that nothing was sent and what to supply. A read that
183+
* Every refusal says that nothing was sent and what to do. A read that
184184
* fails is reported as a failed read, never as "you have no address".
185+
*
186+
* Every refusal is admin copy the settings page shows: it names what the admin
187+
* does (press Send test email signed in as an admin whose account has an email
188+
* address), never a request field or an endpoint. The `to` override is for API
189+
* callers — a non-empty string `to` in the `mail/test` request body wins over
190+
* the account address and skips the account read — and is documented here,
191+
* not in the refusal.
185192
*/
186193
async function resolveMailTestRecipient(
187194
payload: unknown,
@@ -192,8 +199,9 @@ async function resolveMailTestRecipient(
192199
if (typeof named === 'string' && named.trim() !== '') return { to: named.trim() };
193200
if (typeof userId !== 'string' || userId === '') {
194201
return {
195-
refusal: 'No recipient — nothing was sent. This call names no "to" address and carries no signed-in '
196-
+ 'user whose own address the test could go to; pass "to" in the request body.',
202+
refusal: 'No recipient — nothing was sent. The test email goes to the signed-in admin\'s own email '
203+
+ 'address, and no one is signed in on this call. Sign in as an admin whose account has an email '
204+
+ 'address, then press Send test email again.',
197205
};
198206
}
199207
let row: Record<string, unknown> | null;
@@ -206,14 +214,16 @@ async function resolveMailTestRecipient(
206214
} catch (err: any) {
207215
return {
208216
refusal: `Could not read your account's email address to send the test to (${err?.message ?? String(err)}) `
209-
+ '— nothing was sent. Pass "to" in the request body to name a recipient.',
217+
+ '— nothing was sent. The test email goes to the signed-in admin\'s own address; '
218+
+ 'press Send test email again once your account can be read.',
210219
};
211220
}
212221
const email = row?.email;
213222
if (typeof email !== 'string' || email.trim() === '') {
214223
return {
215224
refusal: 'Your account has no email address, and the test is sent to the signed-in admin\'s own address '
216-
+ '— nothing was sent. Add an email address to your account, or pass "to" in the request body.',
225+
+ '— nothing was sent. Press Send test email while signed in as an admin whose account has an email '
226+
+ 'address.',
217227
};
218228
}
219229
return { to: email.trim() };

‎packages/plugins/plugin-email/src/mail-test-recipient.pin.test.ts‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,3 +194,51 @@ describe('served mail/test — the recipient', () => {
194194
expect(nm.sendMail).not.toHaveBeenCalled();
195195
});
196196
});
197+
198+
// Every recipient refusal is admin copy: the settings page shows it beside the
199+
// Send test email button, which has no recipient field. It names what the admin
200+
// does there, never a request field or an endpoint — the `to` override is
201+
// API-caller detail, documented in `resolveMailTestRecipient`'s TSDoc.
202+
describe('served mail/test — the recipient refusals are admin copy', () => {
203+
const adminCopy = (message: unknown) => {
204+
expect(typeof message).toBe('string');
205+
const text = message as string;
206+
expect(text).toContain('nothing was sent');
207+
expect(text).toContain('Send test email');
208+
expect(text).not.toContain('/api/');
209+
expect(text).not.toMatch(/request body/i);
210+
expect(text).not.toMatch(/pass "to"/i);
211+
};
212+
213+
it.each([
214+
['an account with no email address', [{ id: ADMIN_ID, email: null }], {},
215+
'Your account has no email address', 'signed in as an admin whose account has an email address'],
216+
['an account row that is missing', [], {},
217+
'Your account has no email address', 'signed in as an admin whose account has an email address'],
218+
['a failed account read', [{ id: ADMIN_ID, email: ADMIN_EMAIL }], { userReadFails: true },
219+
'Could not read your account\'s email address', 'once your account can be read'],
220+
] as const)('%s', async (_name, users, opts, opening, remedy) => {
221+
const { pressSendTest } = await serve([...users], opts);
222+
223+
const answer = await pressSendTest({});
224+
225+
expect(answer.status).toBe(400);
226+
expect(answer.body?.error?.code).toBe('SETTINGS_ACTION_FAILED');
227+
const message = answer.body?.error?.details?.message;
228+
expect(message).toMatch(new RegExp(`^${opening}`));
229+
expect(message).toContain(remedy);
230+
adminCopy(message);
231+
expect(nm.sendMail).not.toHaveBeenCalled();
232+
});
233+
234+
it('no signed-in user (in-process call)', async () => {
235+
const { settings } = await serve([{ id: ADMIN_ID, email: ADMIN_EMAIL }]);
236+
237+
const result = await settings.runAction('mail', 'test', {}, {});
238+
239+
expect(result.message).toMatch(/^No recipient — nothing was sent\./);
240+
expect(result.message).toContain('Sign in as an admin whose account has an email address');
241+
adminCopy(result.message);
242+
expect(nm.sendMail).not.toHaveBeenCalled();
243+
});
244+
});

0 commit comments

Comments
 (0)