/** * Gmail service – OAuth client, send reply, send ticket-closed email. */ const { google } = require('googleapis'); const { CONFIG } = require('../config'); const { extractRawEmail } = require('../utils'); function getGmailClient() { const auth = new google.auth.OAuth2( process.env.GOOGLE_CLIENT_ID, process.env.GOOGLE_CLIENT_SECRET ); auth.setCredentials({ refresh_token: CONFIG.REFRESH_TOKEN }); return google.gmail({ version: 'v1', auth }); } async function sendGmailReply( threadId, replyText, recipientEmail, subject, discordUser, messageId ) { const gmail = getGmailClient(); const utf8Subject = `=?utf-8?B?${Buffer.from( `Re: ${subject}` ).toString('base64')}?=`; const htmlBody = `

From: ${discordUser} on Discord

${replyText.replace(/\n/g, '
')}


${discordUser}

${CONFIG.SIGNATURE}
`; const headers = [ `From: ${CONFIG.MY_EMAIL}`, `To: ${recipientEmail}`, `Subject: ${utf8Subject}`, messageId ? `In-Reply-To: ${messageId}` : '', messageId ? `References: ${messageId}` : '', 'MIME-Version: 1.0', 'Content-Type: text/html; charset="UTF-8"', '', htmlBody ].filter(Boolean); const raw = Buffer.from(headers.join('\r\n')) .toString('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, ''); await gmail.users.messages.send({ userId: 'me', requestBody: { raw, threadId } }); } async function sendTicketClosedEmail(ticket, discordDisplayName) { try { const gmail = getGmailClient(); // Send to the ticket sender (customer), not derived from thread (which can be support) const recipientEmail = extractRawEmail(ticket.senderEmail || '').toLowerCase(); if (!recipientEmail || recipientEmail === CONFIG.MY_EMAIL) return; let subjectHeader = ticket.subject || 'Support'; let msgId = null; try { const thread = await gmail.users.threads.get({ userId: 'me', 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 = lastMsg.payload.headers.find(h => h.name === 'Message-ID')?.value; } } catch (_) { /* use ticket.subject and no In-Reply-To if thread fetch fails */ } const finalSubject = `${CONFIG.TICKET_CLOSE_SUBJECT_PREFIX} ${subjectHeader}`; const utf8Subject = `=?utf-8?B?${Buffer.from( finalSubject ).toString('base64')}?=`; const serverDisplayName = discordDisplayName || 'Support'; const htmlBody = `

From: ${serverDisplayName} on Discord

Message:

${CONFIG.TICKET_CLOSE_MESSAGE.replace(/\n/g, '
')}

${CONFIG.TICKET_CLOSE_SIGNATURE}


${serverDisplayName}

${CONFIG.SIGNATURE}
`; const rawHeaders = [ `From: ${CONFIG.MY_EMAIL}`, `To: ${recipientEmail}`, `Subject: ${utf8Subject}`, msgId ? `In-Reply-To: ${msgId}` : '', msgId ? `References: ${msgId}` : '', 'MIME-Version: 1.0', 'Content-Type: text/html; charset="UTF-8"', '', htmlBody ].filter(Boolean); const raw = Buffer.from(rawHeaders.join('\r\n')) .toString('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, ''); await gmail.users.messages.send({ userId: 'me', requestBody: { raw, threadId: ticket.gmailThreadId } }); } catch (err) { console.error('Ticket closed email error:', err); } } /** * Send a notification email in the ticket thread (e.g. escalation, high-priority). * @param {Object} ticket - Ticket with gmailThreadId, senderEmail, subject * @param {string} subjectLine - Subject line (e.g. "Ticket escalated" or "Priority updated") * @param {string} messageBody - Plain or HTML message body * @param {string} [fromLabel] - Label for "From" (e.g. "Support on Discord") */ async function sendTicketNotificationEmail(ticket, subjectLine, messageBody, fromLabel) { try { const gmail = getGmailClient(); const recipientEmail = extractRawEmail(ticket.senderEmail || '').toLowerCase(); if (!recipientEmail || recipientEmail === CONFIG.MY_EMAIL) return; let subjectHeader = ticket.subject || 'Support'; let msgId = null; try { const thread = await gmail.users.threads.get({ userId: 'me', 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 = lastMsg.payload.headers.find(h => h.name === 'Message-ID')?.value; } } catch (_) {} const finalSubject = subjectLine || subjectHeader; const utf8Subject = `=?utf-8?B?${Buffer.from(finalSubject).toString('base64')}?=`; const label = fromLabel || CONFIG.SUPPORT_NAME || 'Support'; const htmlBody = `

From: ${label} on Discord

${(messageBody || '').replace(/\n/g, '
')}


${label}

${CONFIG.SIGNATURE}
`; const rawHeaders = [ `From: ${CONFIG.MY_EMAIL}`, `To: ${recipientEmail}`, `Subject: ${utf8Subject}`, msgId ? `In-Reply-To: ${msgId}` : '', msgId ? `References: ${msgId}` : '', 'MIME-Version: 1.0', 'Content-Type: text/html; charset="UTF-8"', '', htmlBody ].filter(Boolean); const raw = Buffer.from(rawHeaders.join('\r\n')) .toString('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, ''); await gmail.users.messages.send({ userId: 'me', requestBody: { raw, threadId: ticket.gmailThreadId } }); } catch (err) { console.error('Ticket notification email error:', err); } } module.exports = { getGmailClient, sendGmailReply, sendTicketClosedEmail, sendTicketNotificationEmail };