Files
sgse-app/packages/backend/convex/email.ts

288 lines
7.9 KiB
TypeScript

import { v } from "convex/values";
import { mutation, query, action, internalMutation } from "./_generated/server";
import { Id } from "./_generated/dataModel";
import { renderizarTemplate } from "./templatesMensagens";
/**
* Enfileirar email para envio
*/
export const enfileirarEmail = mutation({
args: {
destinatario: v.string(), // email
destinatarioId: v.optional(v.id("usuarios")),
assunto: v.string(),
corpo: v.string(),
templateId: v.optional(v.id("templatesMensagens")),
enviadoPorId: v.id("usuarios"),
},
returns: v.object({ sucesso: v.boolean(), emailId: v.optional(v.id("notificacoesEmail")) }),
handler: async (ctx, args) => {
// Validar email
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(args.destinatario)) {
return { sucesso: false };
}
// Adicionar à fila
const emailId = await ctx.db.insert("notificacoesEmail", {
destinatario: args.destinatario,
destinatarioId: args.destinatarioId,
assunto: args.assunto,
corpo: args.corpo,
templateId: args.templateId,
status: "pendente",
tentativas: 0,
enviadoPor: args.enviadoPorId,
criadoEm: Date.now(),
});
return { sucesso: true, emailId };
},
});
/**
* Enviar email usando template
*/
export const enviarEmailComTemplate = mutation({
args: {
destinatario: v.string(),
destinatarioId: v.optional(v.id("usuarios")),
templateCodigo: v.string(),
variaveis: v.any(), // Record<string, string>
enviadoPorId: v.id("usuarios"),
},
returns: v.object({ sucesso: v.boolean(), emailId: v.optional(v.id("notificacoesEmail")) }),
handler: async (ctx, args) => {
// Buscar template
const template = await ctx.db
.query("templatesMensagens")
.withIndex("by_codigo", (q) => q.eq("codigo", args.templateCodigo))
.first();
if (!template) {
console.error("Template não encontrado:", args.templateCodigo);
return { sucesso: false };
}
// Renderizar template
const assunto = renderizarTemplate(template.titulo, args.variaveis);
const corpo = renderizarTemplate(template.corpo, args.variaveis);
// Enfileirar email
const emailId = await ctx.db.insert("notificacoesEmail", {
destinatario: args.destinatario,
destinatarioId: args.destinatarioId,
assunto,
corpo,
templateId: template._id,
status: "pendente",
tentativas: 0,
enviadoPor: args.enviadoPorId,
criadoEm: Date.now(),
});
return { sucesso: true, emailId };
},
});
/**
* Listar emails na fila
*/
export const listarFilaEmails = query({
args: {
status: v.optional(v.union(
v.literal("pendente"),
v.literal("enviando"),
v.literal("enviado"),
v.literal("falha")
)),
limite: v.optional(v.number()),
},
returns: v.array(v.any()),
handler: async (ctx, args) => {
let query = ctx.db.query("notificacoesEmail");
if (args.status) {
query = query.withIndex("by_status", (q) => q.eq("status", args.status));
} else {
query = query.withIndex("by_criado_em");
}
const emails = await query.order("desc").take(args.limite || 100);
return emails;
},
});
/**
* Reenviar email falhado
*/
export const reenviarEmail = mutation({
args: {
emailId: v.id("notificacoesEmail"),
},
returns: v.object({ sucesso: v.boolean() }),
handler: async (ctx, args) => {
const email = await ctx.db.get(args.emailId);
if (!email) {
return { sucesso: false };
}
// Resetar status para pendente
await ctx.db.patch(args.emailId, {
status: "pendente",
tentativas: 0,
ultimaTentativa: undefined,
erroDetalhes: undefined,
});
return { sucesso: true };
},
});
/**
* Action para enviar email REAL usando nodemailer
*/
export const enviarEmailAction = action({
args: {
emailId: v.id("notificacoesEmail"),
},
returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }),
handler: async (ctx, args) => {
"use node";
const nodemailer = require("nodemailer");
try {
// Buscar email da fila
const email = await ctx.runQuery(async (ctx) => {
return await ctx.db.get(args.emailId);
});
if (!email) {
return { sucesso: false, erro: "Email não encontrado" };
}
// Buscar configuração SMTP
const config = await ctx.runQuery(async (ctx) => {
return await ctx.db
.query("configuracaoEmail")
.withIndex("by_ativo", (q) => q.eq("ativo", true))
.first();
});
if (!config) {
return { sucesso: false, erro: "Configuração de email não encontrada ou inativa" };
}
if (!config.testado) {
return { sucesso: false, erro: "Configuração SMTP não foi testada. Teste a conexão primeiro!" };
}
// Marcar como enviando
await ctx.runMutation(async (ctx) => {
await ctx.db.patch(args.emailId, {
status: "enviando",
tentativas: (email.tentativas || 0) + 1,
ultimaTentativa: Date.now(),
});
});
// Criar transporter do nodemailer
const transporter = nodemailer.createTransport({
host: config.smtpHost,
port: config.smtpPort,
secure: config.smtpSecure, // true para 465, false para outros
auth: {
user: config.smtpUser,
pass: config.smtpPassword,
},
tls: {
// Não rejeitar certificados não autorizados (útil para testes)
rejectUnauthorized: false
}
});
// Enviar email REAL
const info = await transporter.sendMail({
from: `"${config.remetenteNome}" <${config.remetenteEmail}>`,
to: email.destinatario,
subject: email.assunto,
html: email.corpo,
});
console.log("✅ Email enviado com sucesso!");
console.log(" Para:", email.destinatario);
console.log(" Assunto:", email.assunto);
console.log(" Message ID:", info.messageId);
// Marcar como enviado
await ctx.runMutation(async (ctx) => {
await ctx.db.patch(args.emailId, {
status: "enviado",
enviadoEm: Date.now(),
});
});
return { sucesso: true };
} catch (error: any) {
console.error("❌ Erro ao enviar email:", error.message);
// Marcar como falha
await ctx.runMutation(async (ctx) => {
const email = await ctx.db.get(args.emailId);
await ctx.db.patch(args.emailId, {
status: "falha",
erroDetalhes: error.message || "Erro desconhecido",
tentativas: (email?.tentativas || 0) + 1,
});
});
return { sucesso: false, erro: error.message || "Erro ao enviar email" };
}
},
});
/**
* Processar fila de emails (cron job - processa emails pendentes)
*/
export const processarFilaEmails = internalMutation({
args: {},
returns: v.object({ processados: v.number() }),
handler: async (ctx) => {
// Buscar emails pendentes (max 10 por execução)
const emailsPendentes = await ctx.db
.query("notificacoesEmail")
.withIndex("by_status", (q) => q.eq("status", "pendente"))
.take(10);
let processados = 0;
for (const email of emailsPendentes) {
// Verificar se não excedeu tentativas (max 3)
if ((email.tentativas || 0) >= 3) {
await ctx.db.patch(email._id, {
status: "falha",
erroDetalhes: "Número máximo de tentativas excedido",
});
continue;
}
// Agendar envio via action
// IMPORTANTE: Não podemos chamar action diretamente de mutation
// Por isso, usaremos o scheduler
await ctx.scheduler.runAfter(0, "email:enviarEmailAction" as any, {
emailId: email._id,
});
processados++;
}
console.log(`📧 Fila de emails processada: ${processados} emails agendados para envio`);
return { processados };
},
});