Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/25424-email-test-refusal-admin-copy.md
Original file line number Diff line number Diff line change
@@ -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.
20 changes: 15 additions & 5 deletions packages/plugins/plugin-email/src/email-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<string, unknown> | null;
Expand All @@ -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() };
Expand Down
48 changes: 48 additions & 0 deletions packages/plugins/plugin-email/src/mail-test-recipient.pin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
Loading