From 03aaec76c2f4be3cf84b5aa8794784e12875e06c Mon Sep 17 00:00:00 2001 From: dmitriylaukhin Date: Sun, 23 Aug 2026 13:41:33 +0500 Subject: [PATCH] Put the reply text inside question notification emails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The notification only said that the school had answered and linked into the LMS, so a student had to log in to read two lines. Delivered mail plus an unread thread was the common outcome — one such thread was a pre-sale question that never converted. The mail now carries the answer itself (capped at 2000 chars, with a pointer to the thread when longer or when files are attached). Quoted blocks are HTML-escaped, which they were not before. Co-Authored-By: Claude Fable 5 --- src/app/api/questions/[id]/messages/route.ts | 2 ++ src/lib/email.ts | 34 +++++++++++++++++--- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/app/api/questions/[id]/messages/route.ts b/src/app/api/questions/[id]/messages/route.ts index 0494034..3a7577f 100644 --- a/src/app/api/questions/[id]/messages/route.ts +++ b/src/app/api/questions/[id]/messages/route.ts @@ -78,6 +78,8 @@ export async function POST( question.user.name, question.title, id, + text.trim(), + Boolean(safeFiles?.length), ); } else { const staff = await prisma.user.findMany({ diff --git a/src/lib/email.ts b/src/lib/email.ts index 628e448..0ff71a6 100644 --- a/src/lib/email.ts +++ b/src/lib/email.ts @@ -89,11 +89,19 @@ function btn(href: string, label: string) { `; } +function escapeHtml(text: string) { + return text + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} + function quote(text: string) { return `
-

${text.replace(/\n/g, "
")}

+

${escapeHtml(text).replace(/\n/g, "
")}

`; @@ -335,23 +343,39 @@ export async function sendQuestionCreatedEmail( }).catch((e) => console.error("[email] sendQuestionCreatedEmail:", e)); } +// Текст ответа кладём прямо в письмо: раньше письмо звало в LMS «прочитать ответ», +// и ради двух строк человеку надо было авторизоваться — часть студентов так и не +// открывала тред (isRead=false при доставленном письме). +const REPLY_PREVIEW_LIMIT = 2000; + export async function sendQuestionReplyEmail( to: string, studentName: string, questionTitle: string, questionId: string, + replyText: string, + hasFiles = false, ) { const school = await getSchoolName(); + const trimmed = replyText.trim(); + const preview = + trimmed.length > REPLY_PREVIEW_LIMIT + ? `${trimmed.slice(0, REPLY_PREVIEW_LIMIT)}…` + : trimmed; + const truncated = trimmed.length > REPLY_PREVIEW_LIMIT; + await getResend().emails.send({ from: FROM, to, subject: `Ответ на ваш вопрос`, html: base(`

Привет, ${studentName}!

-

Школа ответила на ваш вопрос:

- ${quote(questionTitle)} -

Откройте тред чтобы прочитать ответ:

- ${btn(`${BASE_URL}/questions/${questionId}`, "Читать ответ")} +

Школа ответила на ваш вопрос «${escapeHtml(questionTitle)}»:

+ ${quote(preview)} + ${truncated ? `

Ответ длинный — целиком он в треде.

` : ""} + ${hasFiles ? `

К ответу приложены файлы — открыть их можно в треде.

` : ""} +

Ответить или посмотреть переписку целиком:

+ ${btn(`${BASE_URL}/questions/${questionId}`, "Открыть тред")} `, school), }).catch((e) => console.error("[email] sendQuestionReplyEmail:", e)); }