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
2 changes: 1 addition & 1 deletion src/email/postmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export async function processPostmarkInboundEmail(req: Request, res: Response) {
from: myToEmails[0] ? `"${project?.event?.name || 'CodeDay'}" <${myToEmails[0]}>` : config.email.from,
to: email.FromFull.Email,
subject: `Re: ${email.Subject}`,
html: `<p>No one received your message. It was only sent to this unmonitored mailbox. <strong>You likely forgot to reply-all.</strong></p>\n${project?.event?.emailSignature}\n<br /><br /><blockquote>${email.HtmlBody}</blockquote>`
html: `<p>No one received your message. It was only sent to this unmonitored mailbox. <strong>You likely forgot to reply-all.</strong></p>\n${project?.event?.emailSignature || ''}\n<br /><br /><blockquote>${email.HtmlBody}</blockquote>`
});
return res.send('ok');
}
Expand Down
75 changes: 75 additions & 0 deletions tests/testPostmarkReplyOneLeak.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import 'reflect-metadata';
import test from 'node:test';
import assert from 'node:assert/strict';
import Container from 'typedi';
import { PrismaClient } from '@prisma/client';
import { processPostmarkInboundEmail } from '../src/email/postmark';

const INBOUND_DOMAIN = 'labs.example';
const TRACKING_ADDR = `cid12345+es_abc123@${INBOUND_DOMAIN}`;

function mockRes() {
return {
send: () => 'ok',
status: () => ({ send: () => 'ok' }),
} as any;
}

function makeReq() {
return {
body: {
FromFull: { Email: 'sender@example.com' },
Subject: 'Hello',
HtmlBody: 'ORIGINAL-BODY',
ToFull: [{ Email: TRACKING_ADDR }],
CcFull: [],
BccFull: [],
},
} as any;
}

function installMocks(projectResult: any) {
const captured: { html?: string } = {};
Container.set(PrismaClient as any, {
project: { findUnique: async () => projectResult },
});
Container.set('email', {
sendMail: async (args: any) => {
captured.html = args.html;
return { MessageID: 'mid' };
},
});
return captured;
}

// Guards the reply-one auto-reply body against leaking literal "null"/"undefined"
// when project/event/emailSignature is missing. The `from` field already falls
// back via `|| 'CodeDay'`; this lock the same treatment for the signature in `html`.
test('reply-one to an event with emailSignature = null does not leak literal "null"', async () => {
const captured = await installMocks({ event: { name: 'CodeDay Lab', emailSignature: null } });
await processPostmarkInboundEmail(makeReq(), mockRes());

assert.equal(typeof captured.html, 'string');
assert.ok(!captured.html!.includes('\nnull\n'), 'html must not contain literal "\\nnull\\n"');
assert.ok(!captured.html!.includes('\nundefined\n'), 'html must not contain literal "\\nundefined\\n"');
});

test('reply-one to a deleted project does not leak literal "undefined" and falls back the sender name', async () => {
const captured = await installMocks(null);
await processPostmarkInboundEmail(makeReq(), mockRes());

assert.equal(typeof captured.html, 'string');
assert.ok(!captured.html!.includes('\nnull\n'), 'html must not contain literal "\\nnull\\n"');
assert.ok(!captured.html!.includes('\nundefined\n'), 'html must not contain literal "\\nundefined\\n"');
});

test('a real emailSignature is still rendered verbatim', async () => {
const signature = '-- The CodeDay Lab Team';
const captured = await installMocks({ event: { name: 'CodeDay Lab', emailSignature: signature } });
await processPostmarkInboundEmail(makeReq(), mockRes());

assert.equal(typeof captured.html, 'string');
assert.ok(captured.html!.includes(`\n${signature}\n`), 'signature must be interpolated verbatim');
assert.ok(!captured.html!.includes('\nnull\n'));
assert.ok(!captured.html!.includes('\nundefined\n'));
});
Loading