From b77dd04b15ffc73474a0b72b0fb2e50fe1529cb9 Mon Sep 17 00:00:00 2001 From: admin Date: Sat, 2 May 2026 22:03:19 +0200 Subject: [PATCH] EMail templates rework --- src/lib/email.ts | 53 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/src/lib/email.ts b/src/lib/email.ts index bc4abe9..1ff739b 100644 --- a/src/lib/email.ts +++ b/src/lib/email.ts @@ -11,6 +11,17 @@ * SMTP_PASS — App Password * SMTP_FROM — e.g. "PieCed " * ADMIN_NOTIFICATION_EMAIL — e.g. admin@pieced.ch (optional) + * SUPPORT_CONTACT_EMAIL — e.g. support@pieced.ch (optional) + * Customer-facing address for "have + * questions?" follow-ups in + * transactional emails. The from + * address itself (SMTP_USER) is + * typically a noreply mailbox, so we + * don't tell customers to "reply to + * this email" — instead we point them + * at this monitored address. If + * unset, the contact-prompt line is + * simply omitted from emails. */ import nodemailer from "nodemailer"; @@ -42,6 +53,12 @@ function getFrom(): string { ); } +/** Returns the customer-facing support email address, or null if unset. */ +function getSupportContactEmail(): string | null { + const v = process.env.SUPPORT_CONTACT_EMAIL?.trim(); + return v && v.length > 0 ? v : null; +} + /** * Escape HTML entities to prevent injection in HTML emails. */ @@ -125,6 +142,21 @@ export async function sendRejectionEmail( ` : ""; + const supportEmail = getSupportContactEmail(); + // The customer here is rejected pre-onboarding — they don't yet + // have a portal account, so we can't send them to /support. + // Instead point at the configured support address (if set). + // If unset (e.g. early pilot before a support inbox exists), we + // omit the follow-up line entirely rather than promise something + // that goes nowhere — telling the customer to "reply to this + // email" would be misleading because we send from a noreply box. + const contactLineText = supportEmail + ? `If you have questions or would like to discuss this further, please contact us at ${supportEmail}.` + : ""; + const contactLineHtml = supportEmail + ? `

If you have questions or would like to discuss this further, please contact us at ${escapeHtml(supportEmail)}.

` + : ""; + await getTransporter().sendMail({ from: getFrom(), to, @@ -134,18 +166,20 @@ export async function sendRejectionEmail( "", `Thank you for your interest in PieCed IT. Unfortunately, we were unable to approve your onboarding request for ${companyName} at this time.`, notesBlock, - "If you have questions or would like to discuss this further, please reply to this email.", + contactLineText, "", "Best regards,", "PieCed IT", - ].join("\n"), + ] + .filter((s) => s !== "") + .join("\n"), html: `

Update on your onboarding request

Hello ${safeName},

Thank you for your interest in PieCed IT. Unfortunately, we were unable to approve your onboarding request for ${safeCompany} at this time.

${notesHtml} -

If you have questions or would like to discuss this further, please reply to this email.

+ ${contactLineHtml}

PieCed IT — Hosted on-premises in Switzerland

@@ -237,6 +271,15 @@ export async function sendResumeRejectionEmail( ` : ""; + // The customer has portal access (their tenant exists, they + // just had a resume request rejected), so direct them to the + // support ticket system for follow-up. We never tell them to + // "reply to this email" because the from address is a noreply + // mailbox. + const contactLineText = + "If you have questions, open a support ticket at https://app.pieced.ch/support."; + const contactLineHtml = `

If you have questions, open a support ticket.

`; + await getTransporter().sendMail({ from: getFrom(), to, @@ -248,7 +291,7 @@ export async function sendResumeRejectionEmail( notesBlock, "Your tenant remains suspended. As a reminder, your data is preserved for 60 days from the original cancellation date, after which it will be permanently deleted. You can submit a new reactivation request at any time before then.", "", - "If you have questions, please reply to this email.", + contactLineText, "", "Best regards,", "PieCed IT", @@ -260,7 +303,7 @@ export async function sendResumeRejectionEmail(

Thank you for your reactivation request for ${safeCompany}. Unfortunately, we were unable to approve it at this time.

${notesHtml}

Your tenant remains suspended. As a reminder, your data is preserved for 60 days from the original cancellation date, after which it will be permanently deleted. You can submit a new reactivation request at any time before then.

-

If you have questions, please reply to this email.

+ ${contactLineHtml}

PieCed IT — Hosted on-premises in Switzerland