CentralBackend/services/email/MailService.js
2026-02-18 10:24:04 +01:00

265 lines
9.1 KiB
JavaScript

const { BrevoClient } = require('@getbrevo/brevo');
const fs = require('fs');
const path = require('path');
const { logger } = require('../../middleware/logger');
class MailService {
constructor() {
const rawApiKey = process.env.BREVO_API_KEY;
const apiKey = (rawApiKey || '').trim(); // helps catch whitespace/newline issues
this.brevo = new BrevoClient({ apiKey });
logger.info('MailService:brevo_api_key_loaded', {
present: Boolean(apiKey),
length: apiKey.length,
last4: apiKey.length >= 4 ? apiKey.slice(-4) : null
});
this.sender = {
email: process.env.BREVO_SENDER_EMAIL,
name: process.env.BREVO_SENDER_NAME,
};
this.loginUrl = process.env.LOGIN_URL;
this.templatesDir = path.join(__dirname, '..', '..', 'mailTemplates');
}
// Helper to load and render a template
renderTemplate(templateName, variables, lang = 'en') {
logger.info('MailService.renderTemplate:start', { templateName, lang });
// Supported languages
const supportedLangs = ['en', 'de'];
// Fallback to 'en' if unsupported or missing
const chosenLang = supportedLangs.includes(lang) ? lang : 'en';
const templatePath = path.join(this.templatesDir, chosenLang, templateName);
let template;
try {
template = fs.readFileSync(templatePath, 'utf8');
logger.info('MailService.renderTemplate:template_loaded', { templatePath, lang: chosenLang });
} catch (err) {
// Fallback to English if template not found
if (chosenLang !== 'en') {
const fallbackPath = path.join(this.templatesDir, 'en', templateName);
template = fs.readFileSync(fallbackPath, 'utf8');
logger.warn('MailService.renderTemplate:fallback_to_en', { fallbackPath, lang });
} else {
logger.error('MailService.renderTemplate:error', { templatePath, error: err.message });
throw err;
}
}
template = template.replace(/{{(\w+)}}/g, (_, key) => variables[key] || '');
logger.info('MailService.renderTemplate:success', { templateName, lang: chosenLang });
return template;
}
_extractBrevoErrorDetails(error) {
const status = error?.statusCode ?? error?.response?.status;
const data = error?.body ?? error?.response?.data;
let dataSafe;
try {
dataSafe = typeof data === 'string' ? data.slice(0, 2000) : JSON.parse(JSON.stringify(data));
} catch (_) {
dataSafe = '[unserializable_response_data]';
}
return { status, data: dataSafe };
}
async sendRegistrationEmail({ email, firstName, lastName, userType, companyName, lang }) {
logger.info('MailService.sendRegistrationEmail:start', { email, userType, lang });
let subject, text;
const chosenLang = lang || 'en';
if (userType === 'personal') {
subject = chosenLang === 'de'
? `Willkommen bei ProfitPlanet, ${firstName}!`
: `Welcome to ProfitPlanet, ${firstName}!`;
text = this.renderTemplate('registrationPersonal.txt', {
firstName,
lastName,
loginUrl: this.loginUrl
}, chosenLang);
} else if (userType === 'company') {
subject = chosenLang === 'de'
? `Willkommen bei ProfitPlanet, ${companyName}!`
: `Welcome to ProfitPlanet, ${companyName}!`;
text = this.renderTemplate('registrationCompany.txt', {
companyName,
loginUrl: this.loginUrl
}, chosenLang);
} else {
subject = chosenLang === 'de'
? 'Willkommen bei ProfitPlanet!'
: 'Welcome to ProfitPlanet!';
text = chosenLang === 'de'
? `Danke für Ihre Registrierung bei ProfitPlanet!\nSie können sich jetzt hier anmelden: ${this.loginUrl}\n\nMit freundlichen Grüßen,\nProfitPlanet Team`
: `Thank you for registering at ProfitPlanet!\nYou can now log in here: ${this.loginUrl}\n\nBest regards,\nProfitPlanet Team`;
}
try {
const payload = {
sender: this.sender,
to: [{ email }],
subject,
textContent: text
};
const data = await this.brevo.transactionalEmails.sendTransacEmail(payload);
logger.info('MailService.sendRegistrationEmail:email_sent', { email, userType, lang: chosenLang });
return data;
} catch (error) {
const brevoError = this._extractBrevoErrorDetails(error);
logger.error('MailService.sendRegistrationEmail:error', {
email,
userType,
lang: chosenLang,
message: error?.message,
brevoStatus: brevoError.status,
brevoData: brevoError.data
});
throw error;
}
}
async sendVerificationCodeEmail({ email, code, expiresAt, lang }) {
logger.info('MailService.sendVerificationCodeEmail:start', { email, expiresAt, lang }); // don't log code
const chosenLang = lang || 'en';
const subject = chosenLang === 'de'
? 'Ihr ProfitPlanet E-Mail-Verifizierungscode'
: 'Your ProfitPlanet Email Verification Code';
const text = this.renderTemplate('verificationCode.txt', {
code,
expiresAt: expiresAt.toLocaleTimeString()
}, chosenLang);
try {
const payload = {
sender: this.sender,
to: [{ email }],
subject,
textContent: text
};
const data = await this.brevo.transactionalEmails.sendTransacEmail(payload);
logger.info('MailService.sendVerificationCodeEmail:email_sent', { email, lang: chosenLang }); // don't log code
return data;
} catch (error) {
const brevoError = this._extractBrevoErrorDetails(error);
logger.error('MailService.sendVerificationCodeEmail:error', {
email,
lang: chosenLang,
message: error?.message,
brevoStatus: brevoError.status,
brevoData: brevoError.data
});
throw error;
}
}
async sendLoginNotificationEmail({ email, ip, loginTime, userAgent, lang }) {
logger.info('MailService.sendLoginNotificationEmail:start', { email, ip, loginTime, userAgent, lang });
const chosenLang = lang || 'en';
const subject = chosenLang === 'de'
? 'ProfitPlanet: Neue Login-Benachrichtigung'
: 'ProfitPlanet: New Login Notification';
const text = this.renderTemplate('loginNotification.txt', {
email,
ip,
loginTime: loginTime.toLocaleString(),
userAgent
}, chosenLang);
try {
const payload = {
sender: this.sender,
to: [{ email }],
subject,
textContent: text
};
const data = await this.brevo.transactionalEmails.sendTransacEmail(payload);
logger.info('MailService.sendLoginNotificationEmail:email_sent', { email, ip, lang: chosenLang });
return data;
} catch (error) {
const brevoError = this._extractBrevoErrorDetails(error);
logger.error('MailService.sendLoginNotificationEmail:error', {
email,
ip,
lang: chosenLang,
message: error?.message,
brevoStatus: brevoError.status,
brevoData: brevoError.data
});
throw error;
}
}
async sendPasswordResetEmail({ email, firstName, companyName, token, lang }) {
logger.info('MailService.sendPasswordResetEmail:start', { email, token, lang });
const chosenLang = lang || 'en';
const subject = chosenLang === 'de'
? 'ProfitPlanet: Passwort zurücksetzen'
: 'ProfitPlanet: Password Reset';
const resetUrl = `${process.env.PASSWORD_RESET_URL || 'https://profit-planet.partners/password-reset-set'}?token=${token}`;
const text = this.renderTemplate('passwordReset.txt', {
firstName: firstName || '',
companyName: companyName || '',
resetUrl
}, chosenLang);
try {
const payload = {
sender: this.sender,
to: [{ email }],
subject,
textContent: text
};
const data = await this.brevo.transactionalEmails.sendTransacEmail(payload);
logger.info('MailService.sendPasswordResetEmail:email_sent', { email, token, lang: chosenLang });
return data;
} catch (error) {
const brevoError = this._extractBrevoErrorDetails(error);
logger.error('MailService.sendPasswordResetEmail:error', {
email,
token,
lang: chosenLang,
message: error?.message,
brevoStatus: brevoError.status,
brevoData: brevoError.data
});
throw error;
}
}
async sendInvoiceEmail({ email, subject, text, html, lang }) {
logger.info('MailService.sendInvoiceEmail:start', { email, lang, hasHtml: Boolean(html) });
try {
const payload = {
sender: this.sender,
to: [{ email }],
subject,
textContent: text
};
if (html) {
payload.htmlContent = html;
}
const data = await this.brevo.transactionalEmails.sendTransacEmail(payload);
logger.info('MailService.sendInvoiceEmail:email_sent', { email, lang, hasHtml: Boolean(html) });
return data;
} catch (error) {
const brevoError = this._extractBrevoErrorDetails(error);
logger.error('MailService.sendInvoiceEmail:error', {
email,
lang,
message: error?.message,
brevoStatus: brevoError.status,
brevoData: brevoError.data
});
throw error;
}
}
}
module.exports = new MailService();