import nodemailer from 'nodemailer'; import handlebars from 'handlebars'; import { readFileSync } from 'fs'; import { join } from 'path'; import type { SMTPConfig } from '~/utils/types'; export interface EmailData { to: string; subject: string; html: string; text?: string; } export interface WelcomeEmailData { firstName: string; lastName: string; verificationLink: string; memberId: string; logoUrl?: string; } export interface VerificationEmailData { firstName: string; verificationLink: string; logoUrl?: string; } export interface PasswordResetEmailData { firstName: string; resetLink: string; logoUrl?: string; } export interface DuesReminderEmailData { firstName: string; lastName: string; amount: number; dueDate: string; iban?: string; accountHolder?: string; logoUrl?: string; } export class EmailService { private transporter: nodemailer.Transporter | null = null; private templates: Map = new Map(); constructor(private config: SMTPConfig) { this.initializeTransporter(); this.preloadTemplates(); } /** * Initialize the nodemailer transporter */ private initializeTransporter(): void { if (!this.config.host || !this.config.port) { console.warn('[EmailService] SMTP configuration incomplete, emails will not be sent'); return; } try { this.transporter = nodemailer.createTransport({ host: this.config.host, port: this.config.port, secure: this.config.secure, // true for 465, false for other ports auth: this.config.username && this.config.password ? { user: this.config.username, pass: this.config.password } : undefined, tls: { rejectUnauthorized: false // Accept self-signed certificates in development } }); console.log('[EmailService] ✅ SMTP transporter initialized'); } catch (error) { console.error('[EmailService] ❌ Failed to initialize SMTP transporter:', error); } } /** * Preload and compile email templates */ private preloadTemplates(): void { const templateNames = ['welcome', 'verification', 'password-reset', 'dues-reminder', 'test']; templateNames.forEach(templateName => { try { const templatePath = join(process.cwd(), 'server/templates', `${templateName}.hbs`); const templateContent = readFileSync(templatePath, 'utf-8'); const compiledTemplate = handlebars.compile(templateContent); this.templates.set(templateName, compiledTemplate); console.log(`[EmailService] ✅ Template '${templateName}' loaded`); } catch (error) { console.warn(`[EmailService] ⚠️ Template '${templateName}' not found or failed to load:`, error); } }); } /** * Get a compiled template by name */ private getTemplate(templateName: string): handlebars.TemplateDelegate | null { return this.templates.get(templateName) || null; } /** * Send a generic email */ private async sendEmail(emailData: EmailData): Promise { if (!this.transporter) { throw new Error('SMTP transporter not initialized'); } const mailOptions = { from: `${this.config.fromName} <${this.config.fromAddress}>`, to: emailData.to, subject: emailData.subject, html: emailData.html, text: emailData.text || undefined }; try { const info = await this.transporter.sendMail(mailOptions); console.log(`[EmailService] ✅ Email sent successfully to ${emailData.to}:`, info.messageId); } catch (error) { console.error(`[EmailService] ❌ Failed to send email to ${emailData.to}:`, error); throw error; } } /** * Send welcome/verification email to new members */ async sendWelcomeEmail(to: string, data: WelcomeEmailData): Promise { const template = this.getTemplate('welcome'); if (!template) { throw new Error('Welcome email template not found'); } const templateData = { ...data, logoUrl: data.logoUrl || `${useRuntimeConfig().public.domain}/MONACOUSA-Flags_376x376.png` }; const html = template(templateData); await this.sendEmail({ to, subject: 'Welcome to MonacoUSA - Please Verify Your Email', html }); console.log(`[EmailService] ✅ Welcome email sent to ${to}`); } /** * Send email verification email */ async sendVerificationEmail(to: string, data: VerificationEmailData): Promise { const template = this.getTemplate('verification'); if (!template) { throw new Error('Verification email template not found'); } const templateData = { ...data, logoUrl: data.logoUrl || `${useRuntimeConfig().public.domain}/MONACOUSA-Flags_376x376.png` }; const html = template(templateData); await this.sendEmail({ to, subject: 'Verify Your Email - MonacoUSA Portal', html }); console.log(`[EmailService] ✅ Verification email sent to ${to}`); } /** * Send password reset email */ async sendPasswordResetEmail(to: string, data: PasswordResetEmailData): Promise { const template = this.getTemplate('password-reset'); if (!template) { throw new Error('Password reset email template not found'); } const templateData = { ...data, logoUrl: data.logoUrl || `${useRuntimeConfig().public.domain}/MONACOUSA-Flags_376x376.png` }; const html = template(templateData); await this.sendEmail({ to, subject: 'Reset Your Password - MonacoUSA Portal', html }); console.log(`[EmailService] ✅ Password reset email sent to ${to}`); } /** * Send membership dues reminder email */ async sendDuesReminderEmail(to: string, data: DuesReminderEmailData): Promise { const template = this.getTemplate('dues-reminder'); if (!template) { throw new Error('Dues reminder email template not found'); } const templateData = { ...data, logoUrl: data.logoUrl || `${useRuntimeConfig().public.domain}/MONACOUSA-Flags_376x376.png` }; const html = template(templateData); await this.sendEmail({ to, subject: `MonacoUSA Membership Dues Reminder - €${data.amount} Due`, html }); console.log(`[EmailService] ✅ Dues reminder email sent to ${to}`); } /** * Send test email to verify SMTP configuration */ async sendTestEmail(to: string): Promise { const template = this.getTemplate('test'); const templateData = { testTime: new Date().toISOString(), logoUrl: `${useRuntimeConfig().public.domain}/MONACOUSA-Flags_376x376.png`, smtpHost: this.config.host, fromAddress: this.config.fromAddress }; let html: string; if (template) { html = template(templateData); } else { // Fallback HTML if template is not available html = ` SMTP Test Email
MonacoUSA

SMTP Test Email

This is a test email from the MonacoUSA Portal email system.

Test Details:

  • Sent at: ${templateData.testTime}
  • SMTP Host: ${templateData.smtpHost}
  • From Address: ${templateData.fromAddress}

If you received this email, your SMTP configuration is working correctly!


MonacoUSA Portal Email System

`; } await this.sendEmail({ to, subject: 'SMTP Configuration Test - MonacoUSA Portal', html, text: `This is a test email from MonacoUSA Portal. Sent at: ${templateData.testTime}` }); console.log(`[EmailService] ✅ Test email sent to ${to}`); } /** * Verify SMTP connection */ async verifyConnection(): Promise { if (!this.transporter) { throw new Error('SMTP transporter not initialized'); } try { await this.transporter.verify(); console.log('[EmailService] ✅ SMTP connection verified'); return true; } catch (error) { console.error('[EmailService] ❌ SMTP connection verification failed:', error); return false; } } /** * Update SMTP configuration and reinitialize transporter */ updateConfig(newConfig: SMTPConfig): void { this.config = newConfig; this.initializeTransporter(); } /** * Close the transporter connection */ close(): void { if (this.transporter) { this.transporter.close(); this.transporter = null; console.log('[EmailService] SMTP connection closed'); } } } // Singleton instance for reuse across the application let emailServiceInstance: EmailService | null = null; /** * Get or create EmailService instance with current SMTP config */ export function getEmailService(): EmailService { const { getSMTPConfig } = require('./admin-config'); const config = getSMTPConfig(); if (!emailServiceInstance) { emailServiceInstance = new EmailService(config); } else { // Update config in case it changed emailServiceInstance.updateConfig(config); } return emailServiceInstance; } /** * Create a new EmailService instance with custom config */ export function createEmailService(config: SMTPConfig): EmailService { return new EmailService(config); }