monacousa-portal/server/utils/email.ts

357 lines
9.9 KiB
TypeScript

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<string, handlebars.TemplateDelegate> = 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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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 = `
<!DOCTYPE html>
<html>
<head><title>SMTP Test Email</title></head>
<body style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="text-align: center; margin-bottom: 30px;">
<img src="${templateData.logoUrl}" alt="MonacoUSA" style="width: 100px;">
<h1 style="color: #a31515;">SMTP Test Email</h1>
</div>
<p>This is a test email from the MonacoUSA Portal email system.</p>
<div style="background: #f5f5f5; padding: 15px; border-radius: 8px; margin: 20px 0;">
<p><strong>Test Details:</strong></p>
<ul>
<li>Sent at: ${templateData.testTime}</li>
<li>SMTP Host: ${templateData.smtpHost}</li>
<li>From Address: ${templateData.fromAddress}</li>
</ul>
</div>
<p>If you received this email, your SMTP configuration is working correctly!</p>
<hr style="margin: 30px 0;">
<p style="color: #666; font-size: 14px; text-align: center;">
MonacoUSA Portal Email System
</p>
</body>
</html>
`;
}
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<boolean> {
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);
}