Files
PANEL_BASES_ANEXO24/src/lib/server/email-service.ts
hreyes 1b198953d4
Some checks failed
Aduanasoft/PANEL_BASES_ANEXO24/pipeline/head There was a failure building this commit
feature/asignacion-masiva-restauradores (#13)
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>
2026-07-02 14:57:45 +00:00

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>&copy; 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}