+
+
Thanks for your feedback!
+
We really appreciate you taking the time to help improve TigerType. Here's a quick summary:
+
+ ${safeMessage ? `
+
+ |
+ Your message
+ ${escapeHtml(safeMessage)}
+ |
+
+
+
We'll take a look and follow up if we need any more details.
` : ''}
+
— TigerType Team
+
+
Reply to this email to continue the conversation with the TigerType team.
+
`;
+
+ const text = [
+ 'Thanks for your feedback!',
+ '',
+ `We received your ${category || 'feedback'} and will look into it shortly.`,
+ '',
+ 'Summary:',
+ pagePath ? `• Page: ${pagePath}` : null,
+ contactInfo ? `• Contact: ${contactInfo}` : null,
+ `• Submitted: ${submittedLocal} ET`,
+ '',
+ safeMessage ? 'Your message:' : null,
+ safeMessage || null,
+ '',
+ '— TigerType Team'
+ ].filter(Boolean).join('\n');
+
+ // Set Reply-To to team addresses (configurable)
+ const replyTo = process.env.FEEDBACK_REPLY_TO || process.env.FEEDBACK_EMAIL_TO_TEAM;
+
+ await sendMailGeneric({ from, to, subject, text, html, replyTo });
+};
+
+const sendFeedbackEmails = async ({
+ category,
+ message,
+ contactInfo,
+ netid,
+ userAgent,
+ pagePath,
+ createdAt,
+ ackTo // optional: explicit recipient for acknowledgement (null/undefined to suppress)
+}) => {
+ const from = process.env.FEEDBACK_EMAIL_FROM;
+ const teamList = (process.env.FEEDBACK_EMAIL_TO_TEAM || '')
+ .split(',')
+ .map(s => s.trim())
+ .filter(Boolean);
+
+ // acknowledgement recipient policy: only send when explicitly provided (e.g., authenticated user's email)
+ const toUser = (typeof ackTo !== 'undefined') ? ackTo : deriveUserEmail(contactInfo, netid);
+
+ // send acknowledgement first; if it fails, throw
+ if (toUser) {
+ await sendFeedbackAcknowledgement({
+ to: toUser,
+ from,
+ category,
+ message,
+ contactInfo,
+ pagePath,
+ createdAt
+ });
+ }
+
+ // send to team recipients; use first as "to" and rest as "cc" so replies-all includes everyone
+ if (teamList.length > 0) {
+ const [primaryTeamRecipient, ...ccTeamRecipients] = teamList;
+ await sendFeedbackNotification({
+ category,
+ message,
+ contactInfo,
+ netid,
+ userAgent,
+ pagePath,
+ createdAt,
+ to: primaryTeamRecipient,
+ from,
+ cc: ccTeamRecipients.length > 0 ? ccTeamRecipients.join(', ') : undefined
+ });
+ }
+};
+
+module.exports = {
+ sendFeedbackNotification,
+ sendFeedbackEmails
+};
+
+// utils for HTML escaping
+function escapeHtml(str) {
+ return String(str)
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/\"/g, '"')
+ .replace(/'/g, ''');
+}
+function escapeAttr(str) {
+ return escapeHtml(str).replace(/\n/g, ' ');
+}
+
+function getLogoAttachment() {
+ try {
+ const logoPath = path.join(__dirname, '../../client/src/assets/logos/navbar-logo.png');
+ if (fs.existsSync(logoPath)) {
+ return [{ filename: 'navbar-logo.png', path: logoPath, cid: 'tt-logo' }];
+ }
+ } catch (_) {}
+ return undefined;
+}