Re_Backend/src/services/email.service.ts

254 lines
8.0 KiB
TypeScript

/**
* Email Service
*
* Core email sending service with nodemailer
* Supports both test accounts (preview) and production SMTP
*/
import nodemailer from 'nodemailer';
import logger from '@utils/logger';
interface EmailOptions {
to: string | string[];
subject: string;
html: string;
cc?: string | string[];
bcc?: string | string[];
attachments?: any[];
}
export class EmailService {
private transporter: nodemailer.Transporter | null = null;
private useTestAccount: boolean = false;
private testAccountInfo: any = null;
/**
* Initialize email service
* If SMTP credentials are not configured, uses test account for preview
*/
async initialize(): Promise<void> {
const smtpHost = process.env.SMTP_HOST;
const smtpUser = process.env.SMTP_USER;
const smtpPassword = process.env.SMTP_PASSWORD;
// Check if SMTP is configured
if (!smtpHost || !smtpUser || !smtpPassword) {
logger.warn('⚠️ SMTP not configured - using test account for preview');
await this.initializeTestAccount();
return;
}
// Production SMTP configuration
try {
this.transporter = nodemailer.createTransport({
host: smtpHost,
port: parseInt(process.env.SMTP_PORT || '587'),
secure: process.env.SMTP_SECURE === 'true',
auth: {
user: smtpUser,
pass: smtpPassword
},
pool: true, // Use connection pooling
maxConnections: 5,
maxMessages: 100,
rateDelta: 1000,
rateLimit: 5
});
// Verify connection
await this.transporter.verify();
logger.info('✅ Email service initialized with production SMTP');
this.useTestAccount = false;
} catch (error) {
logger.error('❌ Failed to initialize production SMTP:', error);
logger.warn('⚠️ Falling back to test account');
await this.initializeTestAccount();
}
}
/**
* Initialize test account for preview (no real SMTP needed)
*/
private async initializeTestAccount(): Promise<void> {
try {
this.testAccountInfo = await nodemailer.createTestAccount();
this.transporter = nodemailer.createTransport({
host: this.testAccountInfo.smtp.host,
port: this.testAccountInfo.smtp.port,
secure: this.testAccountInfo.smtp.secure,
auth: {
user: this.testAccountInfo.user,
pass: this.testAccountInfo.pass
}
});
this.useTestAccount = true;
logger.info('✅ Email service initialized with test account (preview mode)');
logger.info(`📧 Test account: ${this.testAccountInfo.user}`);
} catch (error) {
logger.error('❌ Failed to initialize test account:', error);
throw new Error('Email service initialization failed');
}
}
/**
* Send email with retry logic
*/
async sendEmail(options: EmailOptions): Promise<{ messageId: string; previewUrl?: string }> {
if (!this.transporter) {
await this.initialize();
}
// If using test account, check if SMTP credentials are now available and re-initialize
if (this.useTestAccount) {
const smtpHost = process.env.SMTP_HOST;
const smtpUser = process.env.SMTP_USER;
const smtpPassword = process.env.SMTP_PASSWORD;
if (smtpHost && smtpUser && smtpPassword) {
logger.info('📧 SMTP credentials detected - re-initializing email service with production SMTP');
await this.initialize();
}
}
const recipients = Array.isArray(options.to) ? options.to.join(', ') : options.to;
const fromAddress = process.env.EMAIL_FROM || 'RE Flow <noreply@royalenfield.com>';
const mailOptions = {
from: fromAddress,
to: recipients,
cc: options.cc,
bcc: options.bcc,
subject: options.subject,
html: options.html,
attachments: options.attachments
};
// Retry logic
const maxRetries = parseInt(process.env.EMAIL_RETRY_ATTEMPTS || '3');
let lastError: any;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const info = await this.transporter!.sendMail(mailOptions);
if (!info || !info.messageId) {
throw new Error('Email sent but no messageId returned');
}
const result: { messageId: string; previewUrl?: string } = {
messageId: info.messageId
};
// If using test account, generate preview URL
if (this.useTestAccount) {
try {
const previewUrl = nodemailer.getTestMessageUrl(info);
if (previewUrl) {
result.previewUrl = previewUrl;
// Always log to console for visibility
console.log('\n' + '='.repeat(80));
console.log(`📧 EMAIL PREVIEW (${options.subject})`);
console.log(`To: ${recipients}`);
console.log(`Preview URL: ${previewUrl}`);
console.log(`Message ID: ${info.messageId}`);
console.log('='.repeat(80) + '\n');
logger.info(`✅ Email sent (TEST MODE) to ${recipients}`);
logger.info(`📧 Preview URL: ${previewUrl}`);
} else {
logger.warn(`⚠️ Email sent but preview URL not available. Message ID: ${info.messageId}`);
logger.warn(`💡 This can happen if the email service is rate-limited or the message hasn't been processed yet.`);
}
} catch (previewError: any) {
logger.error(`❌ Failed to generate preview URL:`, previewError);
logger.warn(`⚠️ Email was sent successfully (Message ID: ${info.messageId}) but preview URL generation failed.`);
logger.warn(`💡 You can try sending the email again to get a new preview URL.`);
// Don't throw - email was sent successfully, just preview URL failed
}
} else {
logger.info(`✅ Email sent to ${recipients}: ${options.subject}`);
}
return result;
} catch (error) {
lastError = error;
logger.error(`❌ Email send attempt ${attempt}/${maxRetries} failed:`, error);
if (attempt < maxRetries) {
const delay = parseInt(process.env.EMAIL_RETRY_DELAY || '5000') * attempt;
logger.info(`⏳ Retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// All retries failed
logger.error(`❌ Failed to send email after ${maxRetries} attempts:`, lastError);
throw new Error(`Email delivery failed: ${lastError?.message || 'Unknown error'}`);
}
/**
* Send email to multiple recipients (batch)
*/
async sendBatch(emails: EmailOptions[]): Promise<void> {
logger.info(`📧 Sending batch of ${emails.length} emails`);
const batchSize = parseInt(process.env.EMAIL_BATCH_SIZE || '10');
for (let i = 0; i < emails.length; i += batchSize) {
const batch = emails.slice(i, i + batchSize);
await Promise.allSettled(
batch.map(email => this.sendEmail(email))
);
// Small delay between batches to avoid rate limiting
if (i + batchSize < emails.length) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
logger.info(`✅ Batch email sending complete`);
}
/**
* Check if email service is in test mode
*/
isTestMode(): boolean {
return this.useTestAccount;
}
/**
* Get test account info (for preview URLs)
*/
getTestAccountInfo(): any {
return this.testAccountInfo;
}
/**
* Close transporter (cleanup)
*/
async close(): Promise<void> {
if (this.transporter) {
this.transporter.close();
logger.info('📧 Email service closed');
}
}
}
// Singleton instance
export const emailService = new EmailService();
// Initialize on import (will use test account if SMTP not configured)
// Note: If secrets are loaded later, the service will re-initialize automatically
// when sendEmail is called (if SMTP credentials become available)
emailService.initialize().catch(error => {
logger.error('Failed to initialize email service:', error);
});