470 lines
15 KiB
JavaScript

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
});
}
console.log('DEBUG: About to send email via SMTP...');
const info = await this.transporter.sendMail(mailOptions);
console.log('DEBUG: Email sent via SMTP successfully:', info.messageId);
console.log('DEBUG: Email info response:', info.response);
// Log email to database if NocoDB service is available
console.log('DEBUG: About to log email to database...');
try {
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
});
console.log('DEBUG: Successfully logged email to database');
} catch (logError) {
console.error('DEBUG: Failed to log email to database:', logError);
// Continue anyway - don't let logging failure affect email success
}
const successResult = {
success: true,
messageId: info.messageId,
response: info.response,
testMode: testMode,
originalRecipient: testMode ? emailOptions.to : undefined
};
console.log('DEBUG: Returning success result:', successResult);
return successResult;
} 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) {
console.log('DEBUG: sendTemplatedEmail called with template:', templateName);
try {
// Render the template
console.log('DEBUG: About to render template...');
const { html, text } = await emailTemplates.render(templateName, templateVariables);
console.log('DEBUG: Template rendered successfully');
// Prepare email options with rendered content
const mailOptions = {
...emailOptions,
text: text,
html: html
};
// Send the email using existing sendEmail method
console.log('DEBUG: About to call sendEmail from sendTemplatedEmail...');
const result = await this.sendEmail(mailOptions, isTest);
console.log('DEBUG: sendEmail returned result:', result);
return result;
} catch (error) {
console.error('DEBUG: 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) {
console.log('DEBUG: sendCampaignEmail called for recipient:', recipientEmail);
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
};
console.log('DEBUG: About to call sendTemplatedEmail from sendCampaignEmail...');
const result = await this.sendTemplatedEmail('campaign-email', templateVariables, emailOptions);
console.log('DEBUG: sendCampaignEmail received result:', result);
return result;
}
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 sendEmailVerification(recipientEmail, verificationUrl, userName) {
try {
const templateVariables = {
USER_NAME: userName || 'there',
VERIFICATION_URL: verificationUrl
};
const emailOptions = {
to: recipientEmail,
from: {
email: process.env.SMTP_FROM_EMAIL,
name: process.env.SMTP_FROM_NAME
},
subject: 'Verify Your Email to Create Your Campaign'
};
return await this.sendTemplatedEmail('email-verification', templateVariables, emailOptions);
} catch (error) {
console.error('Failed to send email verification:', error);
throw error;
}
}
/**
* Send response verification email to representative
* @param {Object} options - Email options
* @param {string} options.representativeEmail - Representative's email address
* @param {string} options.representativeName - Representative's name
* @param {string} options.campaignTitle - Campaign title
* @param {string} options.responseType - Type of response (Email, Letter, etc.)
* @param {string} options.responseText - The actual response text
* @param {string} options.submittedDate - Date the response was submitted
* @param {string} options.submitterName - Name of person who submitted
* @param {string} options.verificationUrl - URL to verify the response
* @param {string} options.reportUrl - URL to report as invalid
*/
async sendResponseVerification(options) {
try {
const {
representativeEmail,
representativeName,
campaignTitle,
responseType,
responseText,
submittedDate,
submitterName,
verificationUrl,
reportUrl
} = options;
const templateVariables = {
REPRESENTATIVE_NAME: representativeName,
CAMPAIGN_TITLE: campaignTitle,
RESPONSE_TYPE: responseType,
RESPONSE_TEXT: responseText,
SUBMITTED_DATE: submittedDate,
SUBMITTER_NAME: submitterName || 'Anonymous',
VERIFICATION_URL: verificationUrl,
REPORT_URL: reportUrl,
APP_NAME: process.env.APP_NAME || 'BNKops Influence',
TIMESTAMP: new Date().toLocaleString()
};
const emailOptions = {
to: representativeEmail,
from: {
email: process.env.SMTP_FROM_EMAIL,
name: process.env.SMTP_FROM_NAME
},
subject: `Response Verification Request - ${campaignTitle}`
};
return await this.sendTemplatedEmail('response-verification', templateVariables, emailOptions);
} catch (error) {
console.error('Failed to send response verification email:', error);
throw error;
}
}
}
module.exports = new EmailService();