sistema-de-chamados/convex/ticketNotifications.ts
2025-12-13 12:38:08 -03:00

168 lines
5.4 KiB
TypeScript

"use node"
import tls from "tls"
import { action } from "./_generated/server"
import { v } from "convex/values"
import { buildBaseUrl, renderSimpleNotificationEmail } from "./emailTemplates"
function b64(input: string) {
return Buffer.from(input, "utf8").toString("base64")
}
type SmtpConfig = { host: string; port: number; username: string; password: string; from: string }
function buildSmtpConfig(): SmtpConfig | null {
const host = process.env.SMTP_ADDRESS || process.env.SMTP_HOST
const port = Number(process.env.SMTP_PORT ?? 465)
const username = process.env.SMTP_USERNAME || process.env.SMTP_USER
const password = process.env.SMTP_PASSWORD || process.env.SMTP_PASS
const legacyFrom = process.env.MAILER_SENDER_EMAIL
const fromEmail = process.env.SMTP_FROM_EMAIL
const fromName = process.env.SMTP_FROM_NAME || "Raven"
const from = legacyFrom || (fromEmail ? `"${fromName}" <${fromEmail}>` : "Raven <no-reply@example.com>")
if (!host || !username || !password) return null
return { host, port, username, password, from }
}
async function sendSmtpMail(cfg: SmtpConfig, to: string, subject: string, html: string) {
return new Promise<void>((resolve, reject) => {
const socket = tls.connect(cfg.port, cfg.host, { rejectUnauthorized: false }, () => {
let buffer = ""
const send = (line: string) => socket.write(line + "\r\n")
const wait = (expected: string | RegExp) =>
new Promise<void>((res) => {
const onData = (data: Buffer) => {
buffer += data.toString()
const lines = buffer.split(/\r?\n/)
const last = lines.filter(Boolean).slice(-1)[0] ?? ""
if (typeof expected === "string" ? last.startsWith(expected) : expected.test(last)) {
socket.removeListener("data", onData)
res()
}
}
socket.on("data", onData)
socket.on("error", reject)
})
;(async () => {
await wait(/^220 /)
send(`EHLO ${cfg.host}`)
await wait(/^250-/)
await wait(/^250 /)
send("AUTH LOGIN")
await wait(/^334 /)
send(b64(cfg.username))
await wait(/^334 /)
send(b64(cfg.password))
await wait(/^235 /)
send(`MAIL FROM:<${cfg.from.match(/<(.+)>/)?.[1] ?? cfg.from}>`)
await wait(/^250 /)
send(`RCPT TO:<${to}>`)
await wait(/^250 /)
send("DATA")
await wait(/^354 /)
const headers = [
`From: ${cfg.from}`,
`To: ${to}`,
`Subject: ${subject}`,
"MIME-Version: 1.0",
"Content-Type: text/html; charset=UTF-8",
].join("\r\n")
send(headers + "\r\n\r\n" + html + "\r\n.")
await wait(/^250 /)
send("QUIT")
socket.end()
resolve()
})().catch(reject)
})
socket.on("error", reject)
})
}
export const sendPublicCommentEmail = action({
args: {
to: v.string(),
ticketId: v.string(),
reference: v.number(),
subject: v.string(),
},
handler: async (_ctx, { to, ticketId, reference, subject }) => {
const smtp = buildSmtpConfig()
if (!smtp) {
console.warn("SMTP not configured; skipping ticket comment email")
return { skipped: true }
}
const baseUrl = buildBaseUrl()
const url = `${baseUrl}/portal/tickets/${ticketId}`
const mailSubject = `Atualização no chamado #${reference}: ${subject}`
const html = renderSimpleNotificationEmail({
title: `Nova atualização no seu chamado #${reference}`,
message: `Um novo comentário foi adicionado ao chamado “${subject}”. Clique abaixo para visualizar e responder pelo portal.`,
ctaLabel: "Abrir e responder",
ctaUrl: url,
})
await sendSmtpMail(smtp, to, mailSubject, html)
return { ok: true }
},
})
export const sendResolvedEmail = action({
args: {
to: v.string(),
ticketId: v.string(),
reference: v.number(),
subject: v.string(),
},
handler: async (_ctx, { to, ticketId, reference, subject }) => {
const smtp = buildSmtpConfig()
if (!smtp) {
console.warn("SMTP not configured; skipping ticket resolution email")
return { skipped: true }
}
const baseUrl = buildBaseUrl()
const url = `${baseUrl}/portal/tickets/${ticketId}`
const mailSubject = `Seu chamado #${reference} foi encerrado`
const html = renderSimpleNotificationEmail({
title: `Chamado #${reference} encerrado`,
message: `O chamado “${subject}” foi marcado como concluído. Caso necessário, você pode responder pelo portal para reabrir dentro do prazo.`,
ctaLabel: "Ver detalhes",
ctaUrl: url,
})
await sendSmtpMail(smtp, to, mailSubject, html)
return { ok: true }
},
})
export const sendAutomationEmail = action({
args: {
to: v.array(v.string()),
subject: v.string(),
html: v.string(),
},
handler: async (_ctx, { to, subject, html }) => {
const smtp = buildSmtpConfig()
if (!smtp) {
console.warn("SMTP not configured; skipping automation email")
return { skipped: true }
}
const recipients = to
.map((email) => email.trim())
.filter(Boolean)
.slice(0, 50)
if (recipients.length === 0) {
return { skipped: true, reason: "no_recipients" }
}
for (const recipient of recipients) {
await sendSmtpMail(smtp, recipient, subject, html)
}
return { ok: true, sent: recipients.length }
},
})