From 5d2df8077bf62e4c7ae9a74ee625700c22330328 Mon Sep 17 00:00:00 2001 From: deyvisonwanderley Date: Tue, 4 Nov 2025 02:14:07 -0300 Subject: [PATCH] feat: enhance email handling with improved error reporting and statistics - Updated the `reenviarEmail` mutation to return detailed error messages for better user feedback. - Added a new query to obtain email queue statistics, providing insights into email statuses. - Enhanced the `processarFilaEmails` mutation to track processing failures and successes more effectively. - Implemented a manual email processing mutation for immediate testing and control over email sending. - Improved email validation and error handling in the email sending action, ensuring robust delivery processes. --- packages/backend/convex/actions/email.ts | 24 ++- packages/backend/convex/actions/smtp.ts | 5 +- packages/backend/convex/email.ts | 197 +++++++++++++++++++++-- 3 files changed, 209 insertions(+), 17 deletions(-) diff --git a/packages/backend/convex/actions/email.ts b/packages/backend/convex/actions/email.ts index 8f27bfa..3ae0249 100644 --- a/packages/backend/convex/actions/email.ts +++ b/packages/backend/convex/actions/email.ts @@ -50,28 +50,48 @@ export const enviar = action({ host: config.servidor, port: config.porta, secure: config.usarSSL, + requireTLS: config.usarTLS, auth: { user: config.usuario, pass: config.senha, // Senha já descriptografada }, tls: { - // Permitir certificados autoassinados + // Permitir certificados autoassinados apenas se necessário rejectUnauthorized: false, + ciphers: "SSLv3", }, + connectionTimeout: 10000, // 10 segundos + greetingTimeout: 10000, + socketTimeout: 10000, }); + // Validar email destinatário antes de enviar + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email.destinatario)) { + throw new Error(`Email destinatário inválido: ${email.destinatario}`); + } + // Enviar email const info = await transporter.sendMail({ from: `"${config.nomeRemetente}" <${config.emailRemetente}>`, to: email.destinatario, subject: email.assunto, html: email.corpo, + text: email.corpo.replace(/<[^>]*>/g, ""), // Versão texto para clientes que não suportam HTML }); + interface MessageInfo { + messageId?: string; + response?: string; + } + + const messageInfo = info as MessageInfo; + console.log("✅ Email enviado com sucesso!", { para: email.destinatario, assunto: email.assunto, - messageId: (info as { messageId?: string }).messageId, + messageId: messageInfo.messageId, + response: messageInfo.response, }); // Marcar como enviado diff --git a/packages/backend/convex/actions/smtp.ts b/packages/backend/convex/actions/smtp.ts index 5caf0ec..c68285a 100644 --- a/packages/backend/convex/actions/smtp.ts +++ b/packages/backend/convex/actions/smtp.ts @@ -42,8 +42,11 @@ export const testarConexao = action({ pass: args.senha, }, tls: { - rejectUnauthorized: !args.usarTLS ? false : false, + rejectUnauthorized: false, }, + connectionTimeout: 10000, // 10 segundos + greetingTimeout: 10000, + socketTimeout: 10000, }); // Verificar conexão diff --git a/packages/backend/convex/email.ts b/packages/backend/convex/email.ts index d9d44c8..af8208f 100644 --- a/packages/backend/convex/email.ts +++ b/packages/backend/convex/email.ts @@ -220,11 +220,21 @@ export const reenviarEmail = mutation({ args: { emailId: v.id("notificacoesEmail"), }, - returns: v.object({ sucesso: v.boolean() }), - handler: async (ctx, args): Promise<{ sucesso: boolean }> => { + returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }), + handler: async (ctx, args): Promise<{ sucesso: boolean; erro?: string }> => { const email = await ctx.db.get(args.emailId); if (!email) { - return { sucesso: false }; + return { sucesso: false, erro: "Email não encontrado" }; + } + + // Verificar se o email não foi enviado com sucesso ainda + if (email.status === "enviado") { + return { sucesso: false, erro: "Este email já foi enviado com sucesso" }; + } + + // Verificar se ainda não excedeu o limite de tentativas (max 3) + if ((email.tentativas || 0) >= 3 && email.status !== "falha") { + return { sucesso: false, erro: "Número máximo de tentativas excedido. Crie um novo email." }; } // Resetar status para pendente @@ -235,6 +245,11 @@ export const reenviarEmail = mutation({ erroDetalhes: undefined, }); + // Agendar envio imediato + await ctx.scheduler.runAfter(0, api.actions.email.enviar, { + emailId: args.emailId, + }); + return { sucesso: true }; }, }); @@ -317,6 +332,58 @@ export const buscarEmailsPorIds = query({ }, }); +/** + * Obter estatísticas da fila de emails + */ +export const obterEstatisticasFilaEmails = query({ + args: {}, + returns: v.object({ + total: v.number(), + pendentes: v.number(), + enviando: v.number(), + enviados: v.number(), + falhas: v.number(), + comErro: v.number(), + ultimaExecucaoCron: v.optional(v.number()), + }), + handler: async (ctx) => { + const todosEmails = await ctx.db + .query("notificacoesEmail") + .collect(); + + const estatisticas = { + total: todosEmails.length, + pendentes: 0, + enviando: 0, + enviados: 0, + falhas: 0, + comErro: 0, + }; + + for (const email of todosEmails) { + switch (email.status) { + case "pendente": + estatisticas.pendentes++; + break; + case "enviando": + estatisticas.enviando++; + break; + case "enviado": + estatisticas.enviados++; + break; + case "falha": + estatisticas.falhas++; + if (email.erroDetalhes) { + estatisticas.comErro++; + } + break; + } + } + + return estatisticas; + }, +}); + /** * Listar agendamentos de email do usuário atual */ @@ -452,15 +519,23 @@ export const markEmailFalha = internalMutation({ */ export const processarFilaEmails = internalMutation({ args: {}, - returns: v.object({ processados: v.number() }), + returns: v.object({ processados: v.number(), falhas: v.number() }), handler: async (ctx) => { - // Buscar emails pendentes (max 10 por execução) + // Buscar emails pendentes que não estão agendados para o futuro (max 10 por execução) + const agora = Date.now(); const emailsPendentes = await ctx.db .query("notificacoesEmail") .withIndex("by_status", (q) => q.eq("status", "pendente")) + .filter((q) => + q.or( + q.eq(q.field("agendadaPara"), undefined), + q.lte(q.field("agendadaPara"), agora) + ) + ) .take(10); let processados = 0; + let falhas = 0; for (const email of emailsPendentes) { // Verificar se não excedeu tentativas (max 3) @@ -469,21 +544,115 @@ export const processarFilaEmails = internalMutation({ status: "falha", erroDetalhes: "Número máximo de tentativas excedido", }); + falhas++; continue; } // Agendar envio via action - await ctx.scheduler.runAfter(0, api.actions.email.enviar, { - emailId: email._id, - }); - - processados++; + try { + await ctx.scheduler.runAfter(0, api.actions.email.enviar, { + emailId: email._id, + }); + processados++; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(`Erro ao agendar email ${email._id}:`, errorMessage); + await ctx.db.patch(email._id, { + status: "falha", + erroDetalhes: `Erro ao agendar envio: ${errorMessage}`, + tentativas: (email.tentativas || 0) + 1, + }); + falhas++; + } } - console.log( - `📧 Fila de emails processada: ${processados} emails agendados para envio` - ); + if (processados > 0 || falhas > 0) { + console.log( + `📧 Fila de emails processada: ${processados} emails agendados, ${falhas} falhas` + ); + } - return { processados }; + return { processados, falhas }; + }, +}); + +/** + * Processar fila de emails manualmente (para testes e envio imediato) + */ +export const processarFilaEmailsManual = mutation({ + args: { + limite: v.optional(v.number()), + }, + returns: v.object({ + sucesso: v.boolean(), + processados: v.number(), + falhas: v.number(), + erro: v.optional(v.string()) + }), + handler: async (ctx, args): Promise<{ + sucesso: boolean; + processados: number; + falhas: number; + erro?: string + }> => { + const usuarioAtual = await getUsuarioAutenticado(ctx); + if (!usuarioAtual) { + return { sucesso: false, processados: 0, falhas: 0, erro: "Usuário não autenticado" }; + } + + // Verificar se usuário tem permissão (TI_MASTER ou admin) + const role = await ctx.db.get(usuarioAtual.roleId); + if (!role || (role.nivel !== 0 && role.nivel !== 1)) { + return { sucesso: false, processados: 0, falhas: 0, erro: "Permissão negada" }; + } + + const limite = args.limite || 10; + const agora = Date.now(); + + // Buscar emails pendentes que não estão agendados para o futuro + const emailsPendentes = await ctx.db + .query("notificacoesEmail") + .withIndex("by_status", (q) => q.eq("status", "pendente")) + .filter((q) => + q.or( + q.eq(q.field("agendadaPara"), undefined), + q.lte(q.field("agendadaPara"), agora) + ) + ) + .take(limite); + + let processados = 0; + let falhas = 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", + }); + falhas++; + continue; + } + + // Agendar envio via action + try { + await ctx.scheduler.runAfter(0, api.actions.email.enviar, { + emailId: email._id, + }); + processados++; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(`Erro ao agendar email ${email._id}:`, errorMessage); + await ctx.db.patch(email._id, { + status: "falha", + erroDetalhes: `Erro ao agendar envio: ${errorMessage}`, + tentativas: (email.tentativas || 0) + 1, + }); + falhas++; + } + } + + return { sucesso: true, processados, falhas }; }, });