This commit is contained in:
@@ -11,6 +11,17 @@
|
|||||||
* SMTP_PASS — App Password
|
* SMTP_PASS — App Password
|
||||||
* SMTP_FROM — e.g. "PieCed <noreply@pieced.ch>"
|
* SMTP_FROM — e.g. "PieCed <noreply@pieced.ch>"
|
||||||
* ADMIN_NOTIFICATION_EMAIL — e.g. admin@pieced.ch (optional)
|
* 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";
|
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.
|
* Escape HTML entities to prevent injection in HTML emails.
|
||||||
*/
|
*/
|
||||||
@@ -125,6 +142,21 @@ export async function sendRejectionEmail(
|
|||||||
</div>`
|
</div>`
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
|
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
|
||||||
|
? `<p>If you have questions or would like to discuss this further, please contact us at <a href="mailto:${escapeHtml(supportEmail)}" style="color: #3b82f6;">${escapeHtml(supportEmail)}</a>.</p>`
|
||||||
|
: "";
|
||||||
|
|
||||||
await getTransporter().sendMail({
|
await getTransporter().sendMail({
|
||||||
from: getFrom(),
|
from: getFrom(),
|
||||||
to,
|
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.`,
|
`Thank you for your interest in PieCed IT. Unfortunately, we were unable to approve your onboarding request for ${companyName} at this time.`,
|
||||||
notesBlock,
|
notesBlock,
|
||||||
"If you have questions or would like to discuss this further, please reply to this email.",
|
contactLineText,
|
||||||
"",
|
"",
|
||||||
"Best regards,",
|
"Best regards,",
|
||||||
"PieCed IT",
|
"PieCed IT",
|
||||||
].join("\n"),
|
]
|
||||||
|
.filter((s) => s !== "")
|
||||||
|
.join("\n"),
|
||||||
html: `
|
html: `
|
||||||
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 560px; margin: 0 auto; color: #e0e0e0; background: #1a1a1a; padding: 32px; border-radius: 12px;">
|
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 560px; margin: 0 auto; color: #e0e0e0; background: #1a1a1a; padding: 32px; border-radius: 12px;">
|
||||||
<h2 style="color: #ffffff; margin-top: 0;">Update on your onboarding request</h2>
|
<h2 style="color: #ffffff; margin-top: 0;">Update on your onboarding request</h2>
|
||||||
<p>Hello ${safeName},</p>
|
<p>Hello ${safeName},</p>
|
||||||
<p>Thank you for your interest in PieCed IT. Unfortunately, we were unable to approve your onboarding request for <strong>${safeCompany}</strong> at this time.</p>
|
<p>Thank you for your interest in PieCed IT. Unfortunately, we were unable to approve your onboarding request for <strong>${safeCompany}</strong> at this time.</p>
|
||||||
${notesHtml}
|
${notesHtml}
|
||||||
<p>If you have questions or would like to discuss this further, please reply to this email.</p>
|
${contactLineHtml}
|
||||||
<hr style="border: none; border-top: 1px solid #333; margin: 24px 0;" />
|
<hr style="border: none; border-top: 1px solid #333; margin: 24px 0;" />
|
||||||
<p style="color: #666; font-size: 12px;">PieCed IT — Hosted on-premises in Switzerland</p>
|
<p style="color: #666; font-size: 12px;">PieCed IT — Hosted on-premises in Switzerland</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -237,6 +271,15 @@ export async function sendResumeRejectionEmail(
|
|||||||
</div>`
|
</div>`
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
|
// 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 = `<p>If you have questions, <a href="https://app.pieced.ch/support" style="color: #3b82f6;">open a support ticket</a>.</p>`;
|
||||||
|
|
||||||
await getTransporter().sendMail({
|
await getTransporter().sendMail({
|
||||||
from: getFrom(),
|
from: getFrom(),
|
||||||
to,
|
to,
|
||||||
@@ -248,7 +291,7 @@ export async function sendResumeRejectionEmail(
|
|||||||
notesBlock,
|
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.",
|
"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,",
|
"Best regards,",
|
||||||
"PieCed IT",
|
"PieCed IT",
|
||||||
@@ -260,7 +303,7 @@ export async function sendResumeRejectionEmail(
|
|||||||
<p>Thank you for your reactivation request for <strong>${safeCompany}</strong>. Unfortunately, we were unable to approve it at this time.</p>
|
<p>Thank you for your reactivation request for <strong>${safeCompany}</strong>. Unfortunately, we were unable to approve it at this time.</p>
|
||||||
${notesHtml}
|
${notesHtml}
|
||||||
<p>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.</p>
|
<p>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.</p>
|
||||||
<p>If you have questions, please reply to this email.</p>
|
${contactLineHtml}
|
||||||
<hr style="border: none; border-top: 1px solid #333; margin: 24px 0;" />
|
<hr style="border: none; border-top: 1px solid #333; margin: 24px 0;" />
|
||||||
<p style="color: #666; font-size: 12px;">PieCed IT — Hosted on-premises in Switzerland</p>
|
<p style="color: #666; font-size: 12px;">PieCed IT — Hosted on-premises in Switzerland</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user