diff --git a/.changeset/25424-email-test-refusal-admin-copy.md b/.changeset/25424-email-test-refusal-admin-copy.md new file mode 100644 index 00000000000..683cc03ce8e --- /dev/null +++ b/.changeset/25424-email-test-refusal-admin-copy.md @@ -0,0 +1,12 @@ +--- +'@objectstack/plugin-email': patch +--- + +fix(plugin-email): the Send test email refusals name what the admin does on the page instead of a REST recipe (#25424) + +Clause-②: no +Metadata-protocol: none + +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. + +No route, response shape, status or code changes: only the message text does. A caller that compared these strings verbatim must update its copy. diff --git a/packages/plugins/plugin-email/src/email-plugin.ts b/packages/plugins/plugin-email/src/email-plugin.ts index 6cf912363ed..17e9d2ffaff 100644 --- a/packages/plugins/plugin-email/src/email-plugin.ts +++ b/packages/plugins/plugin-email/src/email-plugin.ts @@ -180,8 +180,15 @@ const ACCOUNT_OBJECT = 'sys_user'; * reaches the caller in the result message, and it is their own address, so * nothing the caller could not already read is disclosed. * - * Every refusal says that nothing was sent and what to supply. A read that + * Every refusal says that nothing was sent and what to do. A read that * fails is reported as a failed read, never as "you have no address". + * + * Every refusal is admin copy the settings page shows: it names what the admin + * does (press Send test email signed in as an admin whose account has an email + * address), never a request field or an endpoint. The `to` override is for API + * callers — a non-empty string `to` in the `mail/test` request body wins over + * the account address and skips the account read — and is documented here, + * not in the refusal. */ async function resolveMailTestRecipient( payload: unknown, @@ -192,8 +199,9 @@ async function resolveMailTestRecipient( if (typeof named === 'string' && named.trim() !== '') return { to: named.trim() }; if (typeof userId !== 'string' || userId === '') { return { - refusal: 'No recipient — nothing was sent. This call names no "to" address and carries no signed-in ' - + 'user whose own address the test could go to; pass "to" in the request body.', + refusal: 'No recipient — nothing was sent. The test email goes to the signed-in admin\'s own email ' + + 'address, and no one is signed in on this call. Sign in as an admin whose account has an email ' + + 'address, then press Send test email again.', }; } let row: Record | null; @@ -206,14 +214,16 @@ async function resolveMailTestRecipient( } catch (err: any) { return { refusal: `Could not read your account's email address to send the test to (${err?.message ?? String(err)}) ` - + '— nothing was sent. Pass "to" in the request body to name a recipient.', + + '— nothing was sent. The test email goes to the signed-in admin\'s own address; ' + + 'press Send test email again once your account can be read.', }; } const email = row?.email; if (typeof email !== 'string' || email.trim() === '') { return { refusal: 'Your account has no email address, and the test is sent to the signed-in admin\'s own address ' - + '— nothing was sent. Add an email address to your account, or pass "to" in the request body.', + + '— nothing was sent. Press Send test email while signed in as an admin whose account has an email ' + + 'address.', }; } return { to: email.trim() }; diff --git a/packages/plugins/plugin-email/src/mail-test-recipient.pin.test.ts b/packages/plugins/plugin-email/src/mail-test-recipient.pin.test.ts index 89485686455..dd8882c5403 100644 --- a/packages/plugins/plugin-email/src/mail-test-recipient.pin.test.ts +++ b/packages/plugins/plugin-email/src/mail-test-recipient.pin.test.ts @@ -194,3 +194,51 @@ describe('served mail/test — the recipient', () => { expect(nm.sendMail).not.toHaveBeenCalled(); }); }); + +// Every recipient refusal is admin copy: the settings page shows it beside the +// Send test email button, which has no recipient field. It names what the admin +// does there, never a request field or an endpoint — the `to` override is +// API-caller detail, documented in `resolveMailTestRecipient`'s TSDoc. +describe('served mail/test — the recipient refusals are admin copy', () => { + const adminCopy = (message: unknown) => { + expect(typeof message).toBe('string'); + const text = message as string; + expect(text).toContain('nothing was sent'); + expect(text).toContain('Send test email'); + expect(text).not.toContain('/api/'); + expect(text).not.toMatch(/request body/i); + expect(text).not.toMatch(/pass "to"/i); + }; + + it.each([ + ['an account with no email address', [{ id: ADMIN_ID, email: null }], {}, + 'Your account has no email address', 'signed in as an admin whose account has an email address'], + ['an account row that is missing', [], {}, + 'Your account has no email address', 'signed in as an admin whose account has an email address'], + ['a failed account read', [{ id: ADMIN_ID, email: ADMIN_EMAIL }], { userReadFails: true }, + 'Could not read your account\'s email address', 'once your account can be read'], + ] as const)('%s', async (_name, users, opts, opening, remedy) => { + const { pressSendTest } = await serve([...users], opts); + + const answer = await pressSendTest({}); + + expect(answer.status).toBe(400); + expect(answer.body?.error?.code).toBe('SETTINGS_ACTION_FAILED'); + const message = answer.body?.error?.details?.message; + expect(message).toMatch(new RegExp(`^${opening}`)); + expect(message).toContain(remedy); + adminCopy(message); + expect(nm.sendMail).not.toHaveBeenCalled(); + }); + + it('no signed-in user (in-process call)', async () => { + const { settings } = await serve([{ id: ADMIN_ID, email: ADMIN_EMAIL }]); + + const result = await settings.runAction('mail', 'test', {}, {}); + + expect(result.message).toMatch(/^No recipient — nothing was sent\./); + expect(result.message).toContain('Sign in as an admin whose account has an email address'); + adminCopy(result.message); + expect(nm.sendMail).not.toHaveBeenCalled(); + }); +});