const nodemailer = require('nodemailer'); const emailTemplates = require('./emailTemplates'); class EmailService { constructor() { this.transporter = null; this.initializeTransporter(); } initializeTransporter() { try { const transporterConfig = { host: process.env.SMTP_HOST, port: parseInt(process.env.SMTP_PORT) || 587, secure: process.env.SMTP_PORT === '465', // true for 465, false for other ports tls: { rejectUnauthorized: false } }; // Add auth if credentials are provided if (process.env.SMTP_USER && process.env.SMTP_PASS) { transporterConfig.auth = { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS }; } this.transporter = nodemailer.createTransport(transporterConfig); console.log('Email transporter initialized successfully'); } catch (error) { console.error('Failed to initialize email transporter:', error); } } async testConnection() { try { if (!this.transporter) { throw new Error('Email transporter not initialized'); } await this.transporter.verify(); return { success: true, message: 'SMTP connection verified successfully' }; } catch (error) { return { success: false, message: 'SMTP connection failed', error: error.message }; } } async sendEmail(emailOptions, isTest = false) { try { if (!this.transporter) { throw new Error('Email transporter not initialized'); } let to = emailOptions.to; let subject = emailOptions.subject; // Test mode - redirect emails and modify subject const testMode = isTest || process.env.EMAIL_TEST_MODE === 'true'; if (testMode) { const originalTo = to; to = process.env.TEST_EMAIL_RECIPIENT || 'admin@example.com'; subject = `[TEST - Original: ${originalTo}] ${subject}`; console.log(`Email redirected from ${originalTo} to ${to} (Test Mode)`); } const mailOptions = { from: `"${emailOptions.from.name}" <${emailOptions.from.email}>`, to: to, replyTo: emailOptions.replyTo, subject: subject, text: emailOptions.text, html: emailOptions.html }; // Log email details in development if (process.env.NODE_ENV === 'development') { console.log('Email Preview:', { to: mailOptions.to, subject: mailOptions.subject, preview: emailOptions.text ? emailOptions.text.substring(0, 200) + '...' : 'No text content', testMode: testMode }); } const info = await this.transporter.sendMail(mailOptions); console.log('Email sent successfully:', info.messageId); // Log email to database if NocoDB service is available await this.logEmailSent({ to: emailOptions.to, // Log original recipient subject: emailOptions.subject, // Log original subject status: 'sent', messageId: info.messageId, testMode: testMode, senderName: emailOptions.from?.name || 'System', senderEmail: emailOptions.from?.email || process.env.SMTP_FROM_EMAIL }); return { success: true, messageId: info.messageId, response: info.response, testMode: testMode, originalRecipient: testMode ? emailOptions.to : undefined }; } catch (error) { console.error('Email send error:', error); // Log failed email attempt await this.logEmailSent({ to: emailOptions.to, subject: emailOptions.subject, status: 'failed', error: error.message, testMode: isTest || process.env.EMAIL_TEST_MODE === 'true', senderName: emailOptions.from?.name || 'System', senderEmail: emailOptions.from?.email || process.env.SMTP_FROM_EMAIL }); return { success: false, error: error.message }; } } async sendBulkEmails(emails) { const results = []; for (const email of emails) { try { const result = await this.sendEmail(email); results.push({ to: email.to, success: result.success, messageId: result.messageId, error: result.error }); // Add a small delay between emails to avoid overwhelming the SMTP server await new Promise(resolve => setTimeout(resolve, 1000)); } catch (error) { results.push({ to: email.to, success: false, error: error.message }); } } return results; } formatEmailTemplate(template, data) { let formattedTemplate = template; // Replace placeholders with actual data Object.keys(data).forEach(key => { const placeholder = `{{${key}}}`; formattedTemplate = formattedTemplate.replace(new RegExp(placeholder, 'g'), data[key]); }); return formattedTemplate; } validateEmailAddress(email) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } async logEmailSent(emailData) { try { // Use the existing logEmailSend method with the correct field structure const nocodbService = require('./nocodb'); if (nocodbService && process.env.NOCODB_TABLE_EMAILS) { await nocodbService.logEmailSend({ recipientEmail: emailData.to, senderName: emailData.senderName || 'System', senderEmail: emailData.senderEmail || process.env.SMTP_FROM_EMAIL, subject: emailData.subject, postalCode: emailData.postalCode || 'N/A', status: emailData.status || 'sent', timestamp: new Date().toISOString(), senderIP: emailData.senderIP || 'localhost' }); } } catch (error) { console.error('Failed to log email:', error); // Don't throw - logging failure shouldn't prevent email sending } } async previewEmail(emailOptions) { // Generate email preview without sending return { to: emailOptions.to, subject: emailOptions.subject, body: emailOptions.text, html: emailOptions.html, from: `"${emailOptions.from.name}" <${emailOptions.from.email}>`, replyTo: emailOptions.replyTo, timestamp: new Date().toISOString(), testMode: process.env.EMAIL_TEST_MODE === 'true', redirectTo: process.env.EMAIL_TEST_MODE === 'true' ? process.env.TEST_EMAIL_RECIPIENT : null }; } // Template-based email methods async sendTemplatedEmail(templateName, templateVariables, emailOptions, isTest = false) { try { // Render the template const { html, text } = await emailTemplates.render(templateName, templateVariables); // Prepare email options with rendered content const mailOptions = { ...emailOptions, text: text, html: html }; // Send the email using existing sendEmail method return await this.sendEmail(mailOptions, isTest); } catch (error) { console.error('Failed to send templated email:', error); return { success: false, error: error.message }; } } async sendRepresentativeEmail(recipientEmail, senderName, senderEmail, subject, message, postalCode, recipientName = null) { // Generate dynamic subject if not provided const finalSubject = subject || `Message from ${senderName} from ${postalCode}`; const templateVariables = { MESSAGE: message, SENDER_NAME: senderName, SENDER_EMAIL: senderEmail, POSTAL_CODE: postalCode, RECIPIENT_NAME: recipientName || 'Representative' }; const emailOptions = { to: recipientEmail, from: { email: process.env.SMTP_FROM_EMAIL, name: process.env.SMTP_FROM_NAME }, replyTo: senderEmail, subject: finalSubject }; return await this.sendTemplatedEmail('representative-contact', templateVariables, emailOptions); } async sendCampaignEmail(recipientEmail, userEmail, userName, postalCode, subject, message, campaignTitle, recipientName = null, recipientLevel = null) { const templateVariables = { MESSAGE: message, USER_NAME: userName, USER_EMAIL: userEmail, POSTAL_CODE: postalCode, CAMPAIGN_TITLE: campaignTitle, RECIPIENT_NAME: recipientName, RECIPIENT_LEVEL: recipientLevel }; const emailOptions = { to: recipientEmail, from: { email: process.env.SMTP_FROM_EMAIL, name: process.env.SMTP_FROM_NAME }, replyTo: userEmail, subject: subject }; return await this.sendTemplatedEmail('campaign-email', templateVariables, emailOptions); } async sendTestEmail(subject, message, testRecipient = null) { const recipient = testRecipient || process.env.TEST_EMAIL_RECIPIENT || 'admin@example.com'; const templateVariables = { MESSAGE: message }; const emailOptions = { to: recipient, from: { email: process.env.SMTP_FROM_EMAIL, name: process.env.SMTP_FROM_NAME }, replyTo: process.env.SMTP_FROM_EMAIL, subject: `[TEST EMAIL] ${subject}` }; return await this.sendTemplatedEmail('test-email', templateVariables, emailOptions, true); } async previewTemplatedEmail(templateName, templateVariables, emailOptions) { try { const { html, text } = await emailTemplates.render(templateName, templateVariables); return { to: emailOptions.to, subject: emailOptions.subject, body: text, html: html, from: `"${emailOptions.from.name}" <${emailOptions.from.email}>`, replyTo: emailOptions.replyTo, timestamp: new Date().toISOString(), testMode: process.env.EMAIL_TEST_MODE === 'true', redirectTo: process.env.EMAIL_TEST_MODE === 'true' ? process.env.TEST_EMAIL_RECIPIENT : null, templateName: templateName, templateVariables: templateVariables }; } catch (error) { console.error('Failed to preview templated email:', error); throw error; } } // User management email methods async sendLoginDetails(user) { try { const baseUrl = process.env.BASE_URL || `http://localhost:${process.env.PORT || 3333}`; const isAdmin = user.admin || user.Admin || false; const templateVariables = { APP_NAME: 'BNKops Influence', USER_NAME: user.Name || user.name || user.Email || user.email, USER_EMAIL: user.Email || user.email, PASSWORD: user.Password || user.password, USER_ROLE: isAdmin ? 'Administrator' : 'User', LOGIN_URL: `${baseUrl}/login.html`, TIMESTAMP: new Date().toLocaleString() }; const emailOptions = { to: user.Email || user.email, from: { email: process.env.SMTP_FROM_EMAIL, name: process.env.SMTP_FROM_NAME }, subject: `Your Login Details - ${templateVariables.APP_NAME}` }; return await this.sendTemplatedEmail('login-details', templateVariables, emailOptions); } catch (error) { console.error('Failed to send login details email:', error); throw error; } } async sendEmail(emailOptions) { try { if (!this.transporter) { throw new Error('Email transporter not initialized'); } const mailOptions = { from: `${process.env.SMTP_FROM_NAME || 'BNKops Influence'} <${process.env.SMTP_FROM_EMAIL || 'noreply@example.com'}>`, to: emailOptions.to, subject: emailOptions.subject, text: emailOptions.text, html: emailOptions.html }; const info = await this.transporter.sendMail(mailOptions); console.log(`Email sent: ${info.messageId}`); return info; } catch (error) { console.error('Failed to send email:', error); throw error; } } } module.exports = new EmailService();