Skip to content

Commit e62c37b

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/project-thread-qwwbnr
2 parents be7daa3 + 0b8dbea commit e62c37b

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
@@ -186,8 +186,15 @@ const ACCOUNT_OBJECT = 'sys_user';
186186
* reaches the caller in the result message, and it is their own address, so
187187
* nothing the caller could not already read is disclosed.
188188
*
189-
* Every refusal says that nothing was sent and what to supply. A read that
189+
* Every refusal says that nothing was sent and what to do. A read that
190190
* fails is reported as a failed read, never as "you have no address".
191+
*
192+
* Every refusal is admin copy the settings page shows: it names what the admin
193+
* does (press Send test email signed in as an admin whose account has an email
194+
* address), never a request field or an endpoint. The `to` override is for API
195+
* callers — a non-empty string `to` in the `mail/test` request body wins over
196+
* the account address and skips the account read — and is documented here,
197+
* not in the refusal.
191198
*/
192199
async function resolveMailTestRecipient(
193200
payload: unknown,
@@ -198,8 +205,9 @@ async function resolveMailTestRecipient(
198205
if (typeof named === 'string' && named.trim() !== '') return { to: named.trim() };
199206
if (typeof userId !== 'string' || userId === '') {
200207
return {
201-
refusal: 'No recipient — nothing was sent. This call names no "to" address and carries no signed-in '
202-
+ 'user whose own address the test could go to; pass "to" in the request body.',
208+
refusal: 'No recipient — nothing was sent. The test email goes to the signed-in admin\'s own email '
209+
+ 'address, and no one is signed in on this call. Sign in as an admin whose account has an email '
210+
+ 'address, then press Send test email again.',
203211
};
204212
}
205213
let row: Record<string, unknown> | null;
@@ -212,14 +220,16 @@ async function resolveMailTestRecipient(
212220
} catch (err: any) {
213221
return {
214222
refusal: `Could not read your account's email address to send the test to (${err?.message ?? String(err)}) `
215-
+ '— nothing was sent. Pass "to" in the request body to name a recipient.',
223+
+ '— nothing was sent. The test email goes to the signed-in admin\'s own address; '
224+
+ 'press Send test email again once your account can be read.',
216225
};
217226
}
218227
const email = row?.email;
219228
if (typeof email !== 'string' || email.trim() === '') {
220229
return {
221230
refusal: 'Your account has no email address, and the test is sent to the signed-in admin\'s own address '
222-
+ '— nothing was sent. Add an email address to your account, or pass "to" in the request body.',
231+
+ '— nothing was sent. Press Send test email while signed in as an admin whose account has an email '
232+
+ 'address.',
223233
};
224234
}
225235
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)