"use node" import tls from "tls" import { action } from "./_generated/server" import { v } from "convex/values" function b64(input: string) { return Buffer.from(input, "utf8").toString("base64") } async function sendSmtpMail(cfg: { host: string; port: number; username: string; password: string; from: string }, to: string, subject: string, html: string) { return new Promise((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((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) }) } function buildBaseUrl() { return process.env.NEXT_PUBLIC_APP_URL || process.env.APP_BASE_URL || "http://localhost:3000" } function emailTemplate({ title, message, ctaLabel, ctaUrl }: { title: string; message: string; ctaLabel: string; ctaUrl: string }) { return `
Raven Raven

${title}

${message}

${ctaLabel}

Se o botão não funcionar, copie e cole esta URL no navegador:
${ctaUrl}

© ${new Date().getFullYear()} Raven — Rever Tecnologia

` } 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 = { host: process.env.SMTP_ADDRESS!, port: Number(process.env.SMTP_PORT ?? 465), username: process.env.SMTP_USERNAME!, password: process.env.SMTP_PASSWORD!, from: process.env.MAILER_SENDER_EMAIL || "Raven ", } if (!smtp.host || !smtp.username || !smtp.password) { 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 = emailTemplate({ 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 = { host: process.env.SMTP_ADDRESS!, port: Number(process.env.SMTP_PORT ?? 465), username: process.env.SMTP_USERNAME!, password: process.env.SMTP_PASSWORD!, from: process.env.MAILER_SENDER_EMAIL || "Raven ", } if (!smtp.host || !smtp.username || !smtp.password) { 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 = emailTemplate({ 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 } }, })