fix: stop HTML-escaping email subject lines

Subjects were compiled with default Handlebars escaping, so the
app-user-feedback subject rendered a title like "Bob's App & Games" as
"Bob's App & Games" — literal entities in the recipient's mail
client. Subjects are plain-text headers, not HTML; compile them with
noEscape. Header safety is unaffected: the transport encodes newlines
and free-form values collapse whitespace upstream.
This commit is contained in:
Nariman Jelveh
2026-08-11 18:30:29 -07:00
parent c2fe813929
commit 3d9691b11a
2 changed files with 31 additions and 1 deletions
@@ -179,6 +179,29 @@ describe('EmailClient — template rendering', () => {
expect(captured.html).toContain('<strong>424242</strong>');
});
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 = '';
+8 -1
View File
@@ -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 (&amp; 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)),
};
}