246 lines
8.5 KiB
JavaScript
246 lines
8.5 KiB
JavaScript
/**
|
||
* Gmail service – OAuth client, send reply, send ticket-closed email.
|
||
*/
|
||
const { google } = require('googleapis');
|
||
const { CONFIG } = require('../config');
|
||
const { extractRawEmail, escapeHtml } = 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 safeUser = escapeHtml(discordUser);
|
||
const safeReply = escapeHtml(replyText).replace(/\n/g, '<br>');
|
||
const safeLogoUrl = escapeHtml(CONFIG.LOGO_URL || '');
|
||
const safeSignature = escapeHtml(CONFIG.SIGNATURE || '');
|
||
const htmlBody = `
|
||
<div style="font-family: sans-serif; font-size: 14px; color: #333;">
|
||
<p><strong>From:</strong> ${safeUser} on Discord</p>
|
||
<p>${safeReply}</p>
|
||
<hr style="border:none; border-top:1px solid #ddd; margin:20px 0;">
|
||
<table border="0" cellpadding="0" cellspacing="0">
|
||
<tr>
|
||
<td style="padding-right: 12px;">
|
||
${safeLogoUrl ? `<img src="${safeLogoUrl}" width="65">` : ''}
|
||
</td>
|
||
<td style="border-left: 1px solid #ddd; padding-left: 12px;">
|
||
<p style="margin: 0; font-weight: bold;">${safeUser}</p>
|
||
<div style="color: #666; font-size: 12px;">${safeSignature}</div>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
</div>`;
|
||
|
||
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 = escapeHtml(discordDisplayName || 'Support');
|
||
const safeLogoUrl = escapeHtml(CONFIG.LOGO_URL || '');
|
||
const safeSignature = escapeHtml(CONFIG.SIGNATURE || '');
|
||
const safeCloseMessage = escapeHtml(CONFIG.TICKET_CLOSE_MESSAGE || '').replace(/\n/g, '<br>');
|
||
const safeCloseSignature = escapeHtml(CONFIG.TICKET_CLOSE_SIGNATURE || '');
|
||
|
||
const htmlBody = `
|
||
<div style="font-family: sans-serif; font-size: 14px; color: #333;">
|
||
<p><strong>From:</strong> ${serverDisplayName} on Discord</p>
|
||
<p><strong>Message:</strong></p>
|
||
<p>${safeCloseMessage}</p>
|
||
<p style="margin-top: 16px;">${safeCloseSignature}</p>
|
||
<hr style="border:none; border-top:1px solid #ddd; margin:20px 0;">
|
||
<table border="0" cellpadding="0" cellspacing="0">
|
||
<tr>
|
||
<td style="padding-right: 12px;">
|
||
${safeLogoUrl ? `<img src="${safeLogoUrl}" width="65">` : ''}
|
||
</td>
|
||
<td style="border-left: 1px solid #ddd; padding-left: 12px;">
|
||
<p style="margin: 0; font-weight: bold;">${serverDisplayName}</p>
|
||
<div style="color: #666; font-size: 12px;">${safeSignature}</div>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
</div>`;
|
||
|
||
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 = escapeHtml(fromLabel || CONFIG.SUPPORT_NAME || 'Support');
|
||
const safeBody = escapeHtml(messageBody || '').replace(/\n/g, '<br>');
|
||
const safeLogoUrl = escapeHtml(CONFIG.LOGO_URL || '');
|
||
const safeSignature = escapeHtml(CONFIG.SIGNATURE || '');
|
||
const htmlBody = `
|
||
<div style="font-family: sans-serif; font-size: 14px; color: #333;">
|
||
<p><strong>From:</strong> ${label} on Discord</p>
|
||
<p>${safeBody}</p>
|
||
<hr style="border:none; border-top:1px solid #ddd; margin:20px 0;">
|
||
<table border="0" cellpadding="0" cellspacing="0">
|
||
<tr>
|
||
<td style="padding-right: 12px;">
|
||
${safeLogoUrl ? `<img src="${safeLogoUrl}" width="65">` : ''}
|
||
</td>
|
||
<td style="border-left: 1px solid #ddd; padding-left: 12px;">
|
||
<p style="margin: 0; font-weight: bold;">${label}</p>
|
||
<div style="color: #666; font-size: 12px;">${safeSignature}</div>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
</div>`;
|
||
|
||
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
|
||
};
|