Fix usuarios page #6
@@ -50,28 +50,48 @@ export const enviar = action({
|
|||||||
host: config.servidor,
|
host: config.servidor,
|
||||||
port: config.porta,
|
port: config.porta,
|
||||||
secure: config.usarSSL,
|
secure: config.usarSSL,
|
||||||
|
requireTLS: config.usarTLS,
|
||||||
auth: {
|
auth: {
|
||||||
user: config.usuario,
|
user: config.usuario,
|
||||||
pass: config.senha, // Senha já descriptografada
|
pass: config.senha, // Senha já descriptografada
|
||||||
},
|
},
|
||||||
tls: {
|
tls: {
|
||||||
// Permitir certificados autoassinados
|
// Permitir certificados autoassinados apenas se necessário
|
||||||
rejectUnauthorized: false,
|
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
|
// Enviar email
|
||||||
const info = await transporter.sendMail({
|
const info = await transporter.sendMail({
|
||||||
from: `"${config.nomeRemetente}" <${config.emailRemetente}>`,
|
from: `"${config.nomeRemetente}" <${config.emailRemetente}>`,
|
||||||
to: email.destinatario,
|
to: email.destinatario,
|
||||||
subject: email.assunto,
|
subject: email.assunto,
|
||||||
html: email.corpo,
|
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!", {
|
console.log("✅ Email enviado com sucesso!", {
|
||||||
para: email.destinatario,
|
para: email.destinatario,
|
||||||
assunto: email.assunto,
|
assunto: email.assunto,
|
||||||
messageId: (info as { messageId?: string }).messageId,
|
messageId: messageInfo.messageId,
|
||||||
|
response: messageInfo.response,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Marcar como enviado
|
// Marcar como enviado
|
||||||
|
|||||||
@@ -42,8 +42,11 @@ export const testarConexao = action({
|
|||||||
pass: args.senha,
|
pass: args.senha,
|
||||||
},
|
},
|
||||||
tls: {
|
tls: {
|
||||||
rejectUnauthorized: !args.usarTLS ? false : false,
|
rejectUnauthorized: false,
|
||||||
},
|
},
|
||||||
|
connectionTimeout: 10000, // 10 segundos
|
||||||
|
greetingTimeout: 10000,
|
||||||
|
socketTimeout: 10000,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Verificar conexão
|
// Verificar conexão
|
||||||
|
|||||||
@@ -220,11 +220,21 @@ export const reenviarEmail = mutation({
|
|||||||
args: {
|
args: {
|
||||||
emailId: v.id("notificacoesEmail"),
|
emailId: v.id("notificacoesEmail"),
|
||||||
},
|
},
|
||||||
returns: v.object({ sucesso: v.boolean() }),
|
returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }),
|
||||||
handler: async (ctx, args): Promise<{ sucesso: boolean }> => {
|
handler: async (ctx, args): Promise<{ sucesso: boolean; erro?: string }> => {
|
||||||
const email = await ctx.db.get(args.emailId);
|
const email = await ctx.db.get(args.emailId);
|
||||||
if (!email) {
|
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
|
// Resetar status para pendente
|
||||||
@@ -235,6 +245,11 @@ export const reenviarEmail = mutation({
|
|||||||
erroDetalhes: undefined,
|
erroDetalhes: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Agendar envio imediato
|
||||||
|
await ctx.scheduler.runAfter(0, api.actions.email.enviar, {
|
||||||
|
emailId: args.emailId,
|
||||||
|
});
|
||||||
|
|
||||||
return { sucesso: true };
|
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
|
* Listar agendamentos de email do usuário atual
|
||||||
*/
|
*/
|
||||||
@@ -452,15 +519,23 @@ export const markEmailFalha = internalMutation({
|
|||||||
*/
|
*/
|
||||||
export const processarFilaEmails = internalMutation({
|
export const processarFilaEmails = internalMutation({
|
||||||
args: {},
|
args: {},
|
||||||
returns: v.object({ processados: v.number() }),
|
returns: v.object({ processados: v.number(), falhas: v.number() }),
|
||||||
handler: async (ctx) => {
|
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
|
const emailsPendentes = await ctx.db
|
||||||
.query("notificacoesEmail")
|
.query("notificacoesEmail")
|
||||||
.withIndex("by_status", (q) => q.eq("status", "pendente"))
|
.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);
|
.take(10);
|
||||||
|
|
||||||
let processados = 0;
|
let processados = 0;
|
||||||
|
let falhas = 0;
|
||||||
|
|
||||||
for (const email of emailsPendentes) {
|
for (const email of emailsPendentes) {
|
||||||
// Verificar se não excedeu tentativas (max 3)
|
// Verificar se não excedeu tentativas (max 3)
|
||||||
@@ -469,21 +544,115 @@ export const processarFilaEmails = internalMutation({
|
|||||||
status: "falha",
|
status: "falha",
|
||||||
erroDetalhes: "Número máximo de tentativas excedido",
|
erroDetalhes: "Número máximo de tentativas excedido",
|
||||||
});
|
});
|
||||||
|
falhas++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Agendar envio via action
|
// Agendar envio via action
|
||||||
|
try {
|
||||||
await ctx.scheduler.runAfter(0, api.actions.email.enviar, {
|
await ctx.scheduler.runAfter(0, api.actions.email.enviar, {
|
||||||
emailId: email._id,
|
emailId: email._id,
|
||||||
});
|
});
|
||||||
|
|
||||||
processados++;
|
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++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (processados > 0 || falhas > 0) {
|
||||||
console.log(
|
console.log(
|
||||||
`📧 Fila de emails processada: ${processados} emails agendados para envio`
|
`📧 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 };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user