import { v } from "convex/values"; import { mutation, query, action, internalMutation, internalQuery, } from "./_generated/server"; import { Id } from "./_generated/dataModel"; import { renderizarTemplate } from "./templatesMensagens"; import { internal, api } from "./_generated/api"; /** * 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.record(v.string(), v.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()), }, // Tipo inferido automaticamente pelo Convex handler: async (ctx, args) => { if (args.status) { const emails = await ctx.db .query("notificacoesEmail") .withIndex("by_status", (q) => q.eq("status", args.status!)) .order("desc") .take(args.limite ?? 100); return emails; } const emails = await ctx.db .query("notificacoesEmail") .withIndex("by_criado_em") .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 (será implementado com nodemailer) * * NOTA: Este é um placeholder. Implementação real requer nodemailer. */ export const getEmailById = internalQuery({ args: { emailId: v.id("notificacoesEmail") }, // Tipo inferido automaticamente pelo Convex handler: async (ctx, args) => { return await ctx.db.get(args.emailId); }, }); export const getActiveEmailConfig = internalQuery({ args: {}, // Tipo inferido automaticamente pelo Convex handler: async (ctx) => { return await ctx.db .query("configuracaoEmail") .withIndex("by_ativo", (q) => q.eq("ativo", true)) .first(); }, }); export const markEmailEnviando = internalMutation({ args: { emailId: v.id("notificacoesEmail") }, returns: v.null(), handler: async (ctx, args) => { const email = await ctx.db.get(args.emailId); if (!email) return null; await ctx.db.patch(args.emailId, { status: "enviando", tentativas: (email.tentativas || 0) + 1, ultimaTentativa: Date.now(), }); return null; }, }); export const markEmailEnviado = internalMutation({ args: { emailId: v.id("notificacoesEmail") }, returns: v.null(), handler: async (ctx, args) => { await ctx.db.patch(args.emailId, { status: "enviado", enviadoEm: Date.now(), }); return null; }, }); export const markEmailFalha = internalMutation({ args: { emailId: v.id("notificacoesEmail"), erro: v.string() }, returns: v.null(), handler: async (ctx, args) => { const email = await ctx.db.get(args.emailId); if (!email) return null; await ctx.db.patch(args.emailId, { status: "falha", erroDetalhes: args.erro, tentativas: (email.tentativas || 0) + 1, }); return null; }, }); 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"; // eslint-disable-next-line @typescript-eslint/no-var-requires const nodemailer = require("nodemailer"); try { // Buscar email da fila const email = await ctx.runQuery(internal.email.getEmailById, { emailId: args.emailId, }); if (!email) { return { sucesso: false, erro: "Email não encontrado" }; } // Buscar configuração SMTP const config = await ctx.runQuery( internal.email.getActiveEmailConfig, {} ); if (!config) { return { sucesso: false, erro: "Configuração de email não encontrada ou inativa", }; } if (!config.testadoEm) { return { sucesso: false, erro: "Configuração SMTP não foi testada. Teste a conexão primeiro!", }; } // Marcar como enviando await ctx.runMutation(internal.email.markEmailEnviando, { emailId: args.emailId, }); // Criar transporter do nodemailer const transporter = nodemailer.createTransport({ host: config.servidor, port: config.porta, secure: config.usarSSL, auth: { user: config.usuario, pass: config.senhaHash, // Note: em produção deve ser descriptografado }, tls: { rejectUnauthorized: false, }, }); // Enviar email REAL const info = await transporter.sendMail({ from: `"${config.nomeRemetente}" <${config.emailRemetente}>`, 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(internal.email.markEmailEnviado, { emailId: args.emailId, }); return { sucesso: true }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error("❌ Erro ao enviar email:", errorMessage); // Marcar como falha await ctx.runMutation(internal.email.markEmailFalha, { emailId: args.emailId, erro: errorMessage, }); return { sucesso: false, erro: errorMessage }; } }, }); /** * 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 com string path 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 }; }, });