Some checks failed
Aduanasoft/PANEL_BASES_ANEXO24/pipeline/head There was a failure building this commit
integraciones de panel de respaldos fallidos, asignacion masiva de restaurador, trackeo de baks para nuevo sistema de cloudrestore Reviewed-on: #13 Co-authored-by: hreyes <hreyes@aduanasoft.com.mx> Co-committed-by: hreyes <hreyes@aduanasoft.com.mx>
163 lines
6.5 KiB
TypeScript
163 lines
6.5 KiB
TypeScript
/**
|
|
* Servicio SMTP genérico. Si SMTP_HOST no está configurado, las funciones no hacen nada.
|
|
* Porta el patrón de backend/core/mail.py de a24c a Node.js/nodemailer.
|
|
*/
|
|
import nodemailer from 'nodemailer';
|
|
|
|
export interface SendEmailOptions {
|
|
toAddrs: string[];
|
|
subject: string;
|
|
plainBody: string;
|
|
htmlBody?: string;
|
|
fromDisplayName?: string;
|
|
highImportance?: boolean;
|
|
}
|
|
|
|
function buildTransporter() {
|
|
const host = (process.env.SMTP_HOST ?? '').trim();
|
|
if (!host) return null;
|
|
|
|
const port = parseInt(process.env.SMTP_PORT ?? '587', 10);
|
|
const user = (process.env.SMTP_USER ?? '').trim();
|
|
const pass = (process.env.SMTP_PASSWORD ?? '').trim();
|
|
const useTls = (process.env.SMTP_USE_TLS ?? 'true').toLowerCase() !== 'false';
|
|
|
|
return nodemailer.createTransport({
|
|
host,
|
|
port,
|
|
secure: port === 465,
|
|
...(port !== 465 && { requireTLS: useTls }),
|
|
...(user && { auth: { user, pass } })
|
|
});
|
|
}
|
|
|
|
export async function sendSmtpEmail(opts: SendEmailOptions): Promise<void> {
|
|
const host = (process.env.SMTP_HOST ?? '').trim();
|
|
if (!host || opts.toAddrs.length === 0) return;
|
|
|
|
const fromAddr = (process.env.SMTP_FROM || process.env.SMTP_USER || '').trim();
|
|
if (!fromAddr) {
|
|
console.warn('[email-service] SMTP_FROM o SMTP_USER debe configurarse para enviar correo');
|
|
return;
|
|
}
|
|
|
|
const transporter = buildTransporter();
|
|
if (!transporter) return;
|
|
|
|
const from = opts.fromDisplayName?.trim()
|
|
? `"${opts.fromDisplayName.trim()}" <${fromAddr}>`
|
|
: fromAddr;
|
|
|
|
const headers: Record<string, string> = {};
|
|
if (opts.highImportance) {
|
|
headers['Importance'] = 'high';
|
|
headers['X-Priority'] = '1';
|
|
headers['X-MSMail-Priority'] = 'High';
|
|
}
|
|
|
|
await transporter.sendMail({
|
|
from,
|
|
to: opts.toAddrs.join(', '),
|
|
subject: opts.subject,
|
|
text: opts.plainBody,
|
|
...(opts.htmlBody ? { html: opts.htmlBody } : {}),
|
|
headers
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// Plantillas de correo — réplica 1:1 del legacy index.php (logo, azul, narrativa
|
|
// por cliente, link SCAIIWeb, pie © TransmitirAS).
|
|
// ============================================================================
|
|
|
|
const LOGO_URL = 'https://aduanasoft.com/wp-content/uploads/2023/12/web50@3x-8.png';
|
|
|
|
const SCAIIWEB_H4 =
|
|
`<h4 style="color:#007bff;text-align:center;">` +
|
|
`Consulta la última sincronización de datos fácilmente desde ` +
|
|
`<a href="https://a24.aduanasoft.com/SCAIIWeb" style="color:#007bff;text-decoration:none;">SCAIIWeb</a>. ` +
|
|
`Inicia sesión y encontrarás esta información en la esquina inferior derecha de la pantalla.</h4>`;
|
|
|
|
const FOOTER =
|
|
`<div style="background-color:#007bff;color:#fff;text-align:center;padding:10px;">` +
|
|
`<small>© 2024 TransmitirAS. Todos los derechos reservados.</small></div>`;
|
|
|
|
/** Envuelve el contenido en el mismo cascarón del legacy (Arial, logo centrado, pie). */
|
|
function renderSyncEmail(inner: string): string {
|
|
return `<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
|
|
<body style="margin:0;padding:0;background:#ffffff;">
|
|
<div style="font-family:Arial,sans-serif;line-height:1.5;color:#333;">
|
|
<div style="text-align:center;margin-bottom:20px;">
|
|
<img src="${LOGO_URL}" alt="Logo" style="max-width:150px;">
|
|
</div>
|
|
${inner}
|
|
${FOOTER}
|
|
</div>
|
|
</body>
|
|
</html>`;
|
|
}
|
|
|
|
/** Fecha en español "28 de junio de 2026, 17:26" (o "No disponible"), como el strftime del legacy. */
|
|
export function formatFechaEs(iso: string | null | undefined): string {
|
|
if (!iso) return 'No disponible';
|
|
// Normaliza microsegundos (a24c manda isoformat con 6 dígitos) a milisegundos.
|
|
const cleaned = String(iso).replace(/(\.\d{3})\d+/, '$1');
|
|
const d = new Date(cleaned);
|
|
if (isNaN(d.getTime())) return 'No disponible';
|
|
const meses = [
|
|
'enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio',
|
|
'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'
|
|
];
|
|
const dd = String(d.getDate()).padStart(2, '0');
|
|
const mm = meses[d.getMonth()];
|
|
const yyyy = d.getFullYear();
|
|
const HH = String(d.getHours()).padStart(2, '0');
|
|
const MM = String(d.getMinutes()).padStart(2, '0');
|
|
return `${dd} de ${mm} de ${yyyy}, ${HH}:${MM}`;
|
|
}
|
|
|
|
/** Correo de alerta de sincronización (kind=overdue), 1:1 con el legacy index.php. */
|
|
export function buildBackupAlertHtml(alerts: BackupAlertRow[]): string {
|
|
const a = alerts[0];
|
|
const nombre = escHtml(a?.clientName ?? a?.visible_name ?? '');
|
|
const bd = escHtml(a?.visible_name ?? '');
|
|
const fecha = escHtml(formatFechaEs(a?.last_restore_date));
|
|
const inner = `
|
|
<h2 style="color:#007bff;text-align:center;">Notificación de Sincronización</h2>
|
|
<p>Estimado <strong>${nombre}</strong>,</p>
|
|
<p>Detectamos que la base de datos <strong>${bd}</strong> no se ha sincronizado correctamente en las últimas 24 horas.</p>
|
|
<p>La última restauración registrada fue: <strong>${fecha}</strong>.</p>
|
|
${SCAIIWEB_H4}
|
|
<p>Por favor, recuerde nunca cerrar la aplicación ni apagar su equipo. Revise la conexión e intente realizar una sincronización manual desde el botón Backup manual, o contacte al soporte técnico si es necesario.</p>`;
|
|
return renderSyncEmail(inner);
|
|
}
|
|
|
|
/** Correo de "sincronización restablecida" (kind=resolved): misma plantilla legacy, mensaje positivo. */
|
|
export function buildBackupResolvedHtml(alerts: BackupAlertRow[]): string {
|
|
const a = alerts[0];
|
|
const nombre = escHtml(a?.clientName ?? a?.visible_name ?? '');
|
|
const bd = escHtml(a?.visible_name ?? '');
|
|
const fecha = escHtml(formatFechaEs(a?.last_restore_date));
|
|
const inner = `
|
|
<h2 style="color:#28a745;text-align:center;">Sincronización Restablecida</h2>
|
|
<p>Estimado <strong>${nombre}</strong>,</p>
|
|
<p>La base de datos <strong>${bd}</strong> volvió a sincronizarse correctamente.</p>
|
|
<p>La última restauración registrada fue: <strong>${fecha}</strong>.</p>
|
|
${SCAIIWEB_H4}
|
|
<p>No se requiere ninguna acción de su parte. Gracias por mantener su equipo y la aplicación en funcionamiento.</p>`;
|
|
return renderSyncEmail(inner);
|
|
}
|
|
|
|
export interface BackupAlertRow {
|
|
visible_name: string;
|
|
clientName: string | null;
|
|
last_restore_date: string | null;
|
|
daysWithout: number | null;
|
|
}
|
|
|
|
function escHtml(s: string): string {
|
|
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
}
|