feat: automações com envio de e-mail

This commit is contained in:
esdrasrenan 2025-12-13 12:38:08 -03:00
parent 469608a10b
commit 58a1ed6b36
6 changed files with 958 additions and 49 deletions

View file

@ -4,11 +4,31 @@ 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")
}
async function sendSmtpMail(cfg: { host: string; port: number; username: string; password: string; from: string }, to: string, subject: string, html: string) {
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 = ""
@ -63,35 +83,6 @@ async function sendSmtpMail(cfg: { host: string; port: number; username: string;
})
}
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 `
<table width="100%" cellpadding="0" cellspacing="0" role="presentation" style="background:#f8fafc;padding:24px 0;font-family:Arial,Helvetica,sans-serif;color:#0f172a;">
<tr>
<td align="center">
<table width="600" cellpadding="0" cellspacing="0" role="presentation" style="background:white;border:1px solid #e2e8f0;border-radius:12px;padding:24px;">
<tr>
<td style="text-align:left;">
<div style="display:flex;align-items:center;gap:12px;">
<img src="${buildBaseUrl()}/logo-raven.png" alt="Raven" style="width:36px;height:36px;border-radius:8px;"/>
<span style="font-weight:700;font-size:18px;">Raven</span>
</div>
<h1 style="font-size:20px;line-height:1.3;margin:16px 0 8px 0;">${title}</h1>
<p style="font-size:14px;line-height:1.6;margin:0 0 16px 0;color:#334155;">${message}</p>
<a href="${ctaUrl}" style="display:inline-block;background:#111827;color:#fff;text-decoration:none;border-radius:10px;padding:10px 16px;font-weight:600;">${ctaLabel}</a>
<p style="font-size:12px;color:#64748b;margin-top:20px;">Se o botão não funcionar, copie e cole esta URL no navegador:<br/><a href="${ctaUrl}" style="color:#0ea5e9;text-decoration:none;">${ctaUrl}</a></p>
</td>
</tr>
</table>
<p style="font-size:12px;color:#94a3b8;margin-top:12px;">&copy; ${new Date().getFullYear()} Raven Rever Tecnologia</p>
</td>
</tr>
</table>`
}
export const sendPublicCommentEmail = action({
args: {
to: v.string(),
@ -100,21 +91,15 @@ export const sendPublicCommentEmail = action({
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 <no-reply@example.com>",
}
if (!smtp.host || !smtp.username || !smtp.password) {
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 = emailTemplate({
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",
@ -133,21 +118,15 @@ export const sendResolvedEmail = action({
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 <no-reply@example.com>",
}
if (!smtp.host || !smtp.username || !smtp.password) {
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 = emailTemplate({
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",
@ -157,3 +136,33 @@ export const sendResolvedEmail = action({
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 }
},
})