import { fileURLToPath } from "url"; import { EmailTransportConfig } from "@/domain/app-settings/app-settings.type.js"; import { ISettingsService } from "@/domain/app-settings/app-settings.service.js"; import { ILogger } from "@/utils/logger.js"; import fs from "node:fs"; import path from "node:path"; import nodemailer from "nodemailer"; import mjml2html from "mjml"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const SERVICE_NAME = "EmailService"; type MjmlFn = typeof mjml2html; type FileSystem = typeof fs; type PathModule = typeof path; type Mailer = typeof nodemailer; type TemplateCompiler = (template: string) => (context: Record) => string; export interface IEmailService { init(): void; buildEmail(template: string, context: Record): Promise; sendEmail(to: string, subject: string, html: string, transportConfig?: EmailTransportConfig): Promise; } export class EmailService implements IEmailService { static SERVICE_NAME = SERVICE_NAME; private settingsService: ISettingsService; private fs: FileSystem; private path: PathModule; private compile: TemplateCompiler; private mjml2html: MjmlFn; private nodemailer: Mailer; private logger: ILogger; private transporter: ReturnType | null = null; private templateLookup: Record) => string) | undefined>; constructor( settingsService: ISettingsService, fs: FileSystem, path: PathModule, compile: TemplateCompiler, mjml2html: MjmlFn, nodemailer: Mailer, logger: ILogger ) { this.settingsService = settingsService; this.fs = fs; this.path = path; this.compile = compile; this.mjml2html = mjml2html; this.nodemailer = nodemailer; this.logger = logger; this.templateLookup = {}; this.init(); } get serviceName() { return EmailService.SERVICE_NAME; } init = () => { const loadTemplate = (templateName: string) => { try { const templatePath = this.path.join(__dirname, `../templates/${templateName}.mjml`); const templateContent = this.fs.readFileSync(templatePath, "utf8"); return this.compile(templateContent); } catch (error: unknown) { this.logger.error({ message: error instanceof Error ? error.message : "Unknown error", service: SERVICE_NAME, method: "loadTemplate", stack: error instanceof Error ? error.stack : undefined, }); } }; this.templateLookup = { welcomeEmailTemplate: loadTemplate("welcomeEmail"), employeeActivationTemplate: loadTemplate("employeeActivation"), noIncidentsThisWeekTemplate: loadTemplate("noIncidentsThisWeek"), passwordResetTemplate: loadTemplate("passwordReset"), testEmailTemplate: loadTemplate("testEmailTemplate"), unifiedNotificationTemplate: loadTemplate("unifiedNotification"), }; }; buildEmail = async (template: string, context: Record) => { try { const mjml = this.templateLookup[template]?.(context); if (!mjml) { throw new Error(`Template ${template} not found`); } const html = await this.mjml2html(mjml); return html.html; } catch (error: unknown) { this.logger.error({ message: error instanceof Error ? error.message : "Unknown error", service: SERVICE_NAME, method: "buildEmail", stack: error instanceof Error ? error.stack : undefined, }); } }; sendEmail = async (to: string, subject: string, html: string, transportConfig?: EmailTransportConfig) => { let config: EmailTransportConfig; if (typeof transportConfig !== "undefined") { config = transportConfig; } else { config = await this.settingsService.getDBSettings(); } const { systemEmailHost, systemEmailPort, systemEmailSecure, systemEmailPool, systemEmailUser, systemEmailAddress, systemEmailDisplayName, systemEmailPassword, systemEmailConnectionHost, systemEmailTLSServername, systemEmailIgnoreTLS, systemEmailRequireTLS, systemEmailRejectUnauthorized, } = config; const emailConfig = { host: systemEmailHost, port: Number(systemEmailPort), secure: systemEmailSecure, auth: { user: systemEmailUser || systemEmailAddress, pass: systemEmailPassword, }, name: systemEmailConnectionHost || "localhost", connectionTimeout: 5000, pool: systemEmailPool, tls: { rejectUnauthorized: systemEmailRejectUnauthorized, ignoreTLS: systemEmailIgnoreTLS, requireTLS: systemEmailRequireTLS, servername: systemEmailTLSServername, }, }; this.transporter = this.nodemailer.createTransport(emailConfig); try { await this.transporter.verify(); } catch (error: unknown) { this.logger.warn({ message: "Email transporter verification failed", service: SERVICE_NAME, method: "verifyTransporter", stack: error instanceof Error ? error.stack : undefined, }); return false; } const trimmedDisplayName = systemEmailDisplayName?.trim(); const from = trimmedDisplayName && systemEmailAddress ? { name: trimmedDisplayName, address: systemEmailAddress } : systemEmailAddress; try { const info = await this.transporter.sendMail({ to: to, from: from, subject: subject, html: html, }); return info.messageId; } catch (error: unknown) { this.logger.error({ message: error instanceof Error ? error.message : "Unknown error", service: SERVICE_NAME, method: "sendEmail", stack: error instanceof Error ? error.stack : undefined, }); } }; }