"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 ") 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((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) }) } 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 } }, })