diff --git a/handlers/buttons.js b/handlers/buttons.js
index a61eccc..cc0fa44 100644
--- a/handlers/buttons.js
+++ b/handlers/buttons.js
@@ -93,16 +93,35 @@ async function handleButton(interaction) {
// --- CLOSE ---
if (interaction.customId === 'close_ticket') {
- const confirmRow = new ActionRowBuilder().addComponents(
- new ButtonBuilder()
- .setCustomId('confirm_close')
- .setLabel('Confirm Close')
- .setStyle(ButtonStyle.Danger),
- new ButtonBuilder()
- .setCustomId('cancel_close')
- .setLabel('Cancel')
- .setStyle(ButtonStyle.Secondary)
- );
+ const isEmailTicket = ticket.gmailThreadId && !ticket.gmailThreadId.startsWith('discord-');
+ const confirmRow = new ActionRowBuilder();
+ if (isEmailTicket) {
+ confirmRow.addComponents(
+ new ButtonBuilder()
+ .setCustomId('confirm_close_with_email')
+ .setLabel('Confirm Close With Email')
+ .setStyle(ButtonStyle.Danger),
+ new ButtonBuilder()
+ .setCustomId('confirm_close_no_email')
+ .setLabel('Confirm Close Without Email')
+ .setStyle(ButtonStyle.Danger),
+ new ButtonBuilder()
+ .setCustomId('cancel_close')
+ .setLabel('Cancel')
+ .setStyle(ButtonStyle.Secondary)
+ );
+ } else {
+ confirmRow.addComponents(
+ new ButtonBuilder()
+ .setCustomId('confirm_close')
+ .setLabel('Confirm Close')
+ .setStyle(ButtonStyle.Danger),
+ new ButtonBuilder()
+ .setCustomId('cancel_close')
+ .setLabel('Cancel')
+ .setStyle(ButtonStyle.Secondary)
+ );
+ }
return interaction.reply({
content: 'Are you sure you want to close this ticket?',
@@ -110,7 +129,12 @@ async function handleButton(interaction) {
});
}
- if (interaction.customId === 'confirm_close') {
+ if (
+ interaction.customId === 'confirm_close' ||
+ interaction.customId === 'confirm_close_with_email' ||
+ interaction.customId === 'confirm_close_no_email'
+ ) {
+ const sendEmail = interaction.customId !== 'confirm_close_no_email';
const timerSeconds = CONFIG.FORCE_CLOSE_TIMER;
if (pendingCloses.has(interaction.channel.id)) {
return interaction.reply({ content: 'A close is already pending for this ticket.', ephemeral: true });
@@ -123,6 +147,7 @@ async function handleButton(interaction) {
);
await interaction.update({ content: `Closing ticket in ${timerSeconds} seconds.`, components: [cancelRow] });
const timerId = setTimeout(async () => {
+ const pending = pendingCloses.get(interaction.channel.id);
pendingCloses.delete(interaction.channel.id);
const freshTicket = await Ticket.findOne({ discordThreadId: interaction.channel.id }).lean();
if (!freshTicket || freshTicket.status === 'closed') return;
@@ -132,9 +157,10 @@ async function handleButton(interaction) {
{ name: 'Set by', value: interaction.user.tag },
{ name: 'Duration', value: `${timerSeconds}s` }
]).catch(() => {});
- await handleConfirmClose(interaction, freshTicket);
+ const effectiveSendEmail = pending?.sendEmail ?? true;
+ await handleConfirmClose(interaction, freshTicket, effectiveSendEmail);
}, timerSeconds * 1000);
- pendingCloses.set(interaction.channel.id, { timeout: timerId, userId: interaction.user.id, username: interaction.user.tag });
+ pendingCloses.set(interaction.channel.id, { timeout: timerId, userId: interaction.user.id, username: interaction.user.tag, sendEmail });
return;
}
@@ -394,7 +420,7 @@ async function handleClaim(interaction, ticket) {
}
// --- CONFIRM CLOSE ---
-async function handleConfirmClose(interaction, ticket) {
+async function handleConfirmClose(interaction, ticket, sendEmail = true) {
const closedAt = new Date();
try {
await interaction.update({ content: 'Archiving and closing...', components: [] });
@@ -522,8 +548,8 @@ async function handleConfirmClose(interaction, ticket) {
const closerDisplayName =
interaction.member?.displayName || interaction.user.username;
- if (!ticket.gmailThreadId?.startsWith('discord-')) {
- await sendTicketClosedEmail(ticket, closerDisplayName);
+ if (sendEmail && !ticket.gmailThreadId?.startsWith('discord-')) {
+ await sendTicketClosedEmail(ticket, closerDisplayName, interaction.user.id);
}
await Ticket.updateOne(
{ gmailThreadId: ticket.gmailThreadId },
diff --git a/services/gmail.js b/services/gmail.js
index 0385528..091ead1 100644
--- a/services/gmail.js
+++ b/services/gmail.js
@@ -74,7 +74,7 @@ async function reloadGmailClient() {
return { emailAddress: profile.data.emailAddress };
}
-async function sendTicketClosedEmail(ticket, discordDisplayName) {
+async function sendTicketClosedEmail(ticket, closerName, userId = null) {
try {
const gmail = getGmailClient();
@@ -86,7 +86,7 @@ async function sendTicketClosedEmail(ticket, discordDisplayName) {
return;
}
- let subjectHeader = ticket.subject || 'Support';
+ let originalSubject = null;
let msgId = null;
try {
const thread = await gmail.users.threads.get({
@@ -94,63 +94,74 @@ async function sendTicketClosedEmail(ticket, discordDisplayName) {
id: ticket.gmailThreadId
});
const messages = thread.data.messages || [];
- const lastMsg = [...messages].reverse()[0];
- if (lastMsg?.payload?.headers) {
- const subj = lastMsg.payload.headers.find(h => h.name === 'Subject')?.value;
- if (subj) subjectHeader = subj;
- msgId = sanitizeHeaderValue(lastMsg.payload.headers.find(h => h.name === 'Message-ID')?.value);
+ const firstMsg = messages[0];
+ if (firstMsg?.payload?.headers) {
+ const subj = firstMsg.payload.headers.find(h => h.name === 'Subject')?.value;
+ if (subj) originalSubject = subj;
+ msgId = sanitizeHeaderValue(firstMsg.payload.headers.find(h => h.name === 'Message-ID')?.value);
}
- } catch (_) {
- /* use ticket.subject and no In-Reply-To if thread fetch fails */
+ } catch (_) {}
+
+ const baseSubject = originalSubject || ticket.subject || 'Support';
+ const stripped = String(baseSubject).replace(/^(?:\s*Re\s*:\s*)+/i, '');
+ const safeSubject = sanitizeHeaderValue(`Re: ${stripped}`);
+ const utf8Subject = `=?utf-8?B?${Buffer.from(safeSubject).toString('base64')}?=`;
+
+ const messageBody = `${closerName} has marked this ticket as resolved. If you would like to re-open this issue, please reply to this email.`;
+
+ let signatureBlocks = { text: '', html: '' };
+ if (userId) {
+ signatureBlocks = await getStaffSignatureBlocks(userId);
}
+ // signatureBlocks.html arrives pre-escaped from getStaffSignatureBlocks.
+ const safeStaffSigHtml = signatureBlocks.html ? signatureBlocks.html.replace(/\n/g, '
') : '';
+ const safeStaffSigText = signatureBlocks.text;
- const finalSubject = sanitizeHeaderValue(`${CONFIG.TICKET_CLOSE_SUBJECT_PREFIX} ${subjectHeader}`);
- const utf8Subject = `=?utf-8?B?${Buffer.from(
- finalSubject
- ).toString('base64')}?=`;
-
- const serverDisplayName = escapeHtml(discordDisplayName || CONFIG.SUPPORT_NAME || 'Support');
- const safeLogoUrl = escapeHtml(CONFIG.LOGO_URL || '');
- const safeSignature = escapeHtml(CONFIG.SIGNATURE || '').replace(/\n/g, '
');
- const safeCloseMessage = escapeHtml(CONFIG.TICKET_CLOSE_MESSAGE || '').replace(/\n/g, '
');
- const safeCloseSignature = escapeHtml(CONFIG.TICKET_CLOSE_SIGNATURE || '').replace(/\n/g, '
');
const htmlBody = `
-
From: ${serverDisplayName} on Discord
-Message:
-${safeCloseMessage}
-${safeCloseSignature}
-|
- ${safeLogoUrl ? ` |
-
- ${serverDisplayName} -${safeSignature}
- |
-
${escapeHtml(messageBody).replace(/\n/g, '
')}
${safeStaffSigHtml}
` : ''} + ${buildCompanySigHtml()} +