diff --git a/src/backend/clients/email/EmailClient.test.ts b/src/backend/clients/email/EmailClient.test.ts index 340c31d91..a409f6e75 100644 --- a/src/backend/clients/email/EmailClient.test.ts +++ b/src/backend/clients/email/EmailClient.test.ts @@ -179,6 +179,29 @@ describe('EmailClient — template rendering', () => { expect(captured.html).toContain('424242'); }); + it('does not HTML-escape values in the plain-text subject header', async () => { + const client = startClient(); + const captured: { subject?: string } = {}; + vi.spyOn(client, 'sendRaw').mockImplementation(async (options) => { + captured.subject = options.subject; + return null; + }); + + await client.send('dev@example.test', 'app-user-feedback', { + owner_username: 'dev', + sender_username: 'user', + sender_email: null, + app_title: "Bob's App & Games", + app_name: 'bobs-app', + app_link: 'https://puter.example/app/bobs-app', + message: 'hi', + }); + + // A subject is not HTML — entities would render literally in the + // recipient's mail client. + expect(captured.subject).toBe("New user feedback for Bob's App & Games"); + }); + it('escapes html and converts newlines in nl2br values', async () => { const client = startClient(); let html = ''; diff --git a/src/backend/clients/email/EmailClient.ts b/src/backend/clients/email/EmailClient.ts index 6548e0652..e12318976 100644 --- a/src/backend/clients/email/EmailClient.ts +++ b/src/backend/clients/email/EmailClient.ts @@ -291,7 +291,14 @@ export class EmailClient extends PuterClient { private compileTemplates(): void { for (const [name, template] of Object.entries(EMAIL_TEMPLATES)) { this.compiledTemplates[name as EmailTemplateName] = { - subject: handlebars.compile(template.subject), + // Subjects are plain-text headers: HTML-escaping would put + // literal entities in front of the recipient (& etc.). + // Header safety is handled elsewhere — the transport encodes + // newlines, and free-form values (e.g. app_title) collapse + // whitespace upstream. + subject: handlebars.compile(template.subject, { + noEscape: true, + }), html: handlebars.compile(dedent(template.html)), }; }