'use strict';
const nodemailer = require('nodemailer'),
Email = require('email-templates'),
hbs = require('handlebars'),
assert = require('assert'),
fs = require('fs-extra'),
cloneDeep = require('clone-deep'),
path = require('path'),
utils = require('./utils'),
env = require('./env'),
{ Errors, DEFAULT_LANG } = require('./constants'),
{ AppInputError, AppAuthError } = require('./app_error'),
debug = require('debug')('agm:mailer');
const TemplateType = Object.freeze({
PUG: 'pug', // default
HANDLEBARS: 'hbs',
});
const Templates = Object.freeze({
RESET_PASSWORD: 'reset-password',
PASSWORD_RESET: 'password-reset',
UPDATE_PAYMENT: 'update-payment',
UPDATE_BILL_ADDRESS: 'update-address',
CURRENT_SUBSCRIPTIONS: 'current-subscriptions',
SUB_RENEWAL_REMIND: 'sub-renewal-remind',
SUB_TRIAL_END_REMIND: 'sub-trial-end-remind',
PROMO_EXPIRED: 'promo-expired',
TEMPORARY_CREDENTIAL: 'temporary-credential',
EMAIL_VERIFICATION: 'email-verification',
NEW_ACCOUNT_WELCOME: 'new-account-welcome'
});
const transporter = nodemailer.createTransport({
host: env.SMTP_HOST,
port: env.SMTP_PORT,
secure: env.SMTP_SECURE,
auth: {
user: env.SMTP_USR,
pass: env.SMTP_PWD
},
// debug: true,
// logger: true
});
function registerHbsPartials(partialsPath) {
// Ref handlebars with vars in templates: https://github.com/forwardemail/email-templates/issues/283
// hbs is your own instance of handlebars, with partials
// Register all available partials under the emails' partials folder
const partialDir = path.join(process.cwd() + partialsPath);
// const partialFiles =
fs.readdir(partialDir, (err, partialFiles) => {
if (!err && !utils.isEmptyArray(partialFiles)) {
for (let i = 0; i < partialFiles.length; i++) {
const pfile = partialFiles[i];
hbs.registerPartial(path.basename(pfile, '.hbs'), require(path.join(partialDir, pfile)));
}
// Override consolidate's handlebars instance
// email.config.views.options.engineSource.requires.handlebars = hbs;
}
});
}
registerHbsPartials('/emails/partials/');
hbs.registerHelper('$t', function (key, options) {
// Translation helper to translate message by key or with hash
const translation = options.data.root.t({ phrase: key, locale: options.data.root.locale }, options.hash);
// Convert newline characters (\n) to
tags for HTML emails
if (translation) {
return new hbs.SafeString(translation.replace(/\n/g, '
'));
}
return '';
});
require('handlebars-helpers')(['comparison']);
const agmMailSig = ['Best,', 'AgMision Team'];
const debugging = false;
const defaultMailOps = {
transport: transporter,
preview: debugging,
send: true,
i18n: {
locales: ['en', 'es', 'pt']
}
};
function init(ops) {
if (ops) {
Object.assign({}, defaultMailOps, ops);
}
}
function _toLocale(locals) {
if (!utils.isEmptyObj(locals) && !utils.hasProperty(locals, 'locale')) {
const _locals = cloneDeep(locals);
_locals.locale = _locals.lang || DEFAULT_LANG;
delete _locals.lang;
return _locals;
}
return locals;
}
function getAppHostUrl(locals) {
if (!locals || !locals.baseUrl) AppInputError.throw();
return `${locals.baseUrl}/${locals.lang || DEFAULT_LANG}/#`;
}
/**
* Send an email with given template and additional info
* @param {*} template the template name
* @param {*} locals additional metadata
* @returns
*/
async function sendMail(template, locals, to, from) {
const email = await createEmail({ from: from, to: to });
return email.send({
template: template,
// message: {
// to: to,
// },
locals: _toLocale(locals)
});
}
/**
* Send a text email from to with the mail options
* @param {*} contentOps { subject: , text: }
*/
async function sendTextMail(contentOps, to, cc) {
if (utils.isEmptyObj(contentOps)) {
debug('Can not send Text email. No contentOps provided !');
return;
}
const _mailOps = Object.assign(contentOps,
{
from: 'AgMission ',
to: to || 'agm_admin@agnav.com'
});
if (cc) _mailOps.cc = cc;
return transporter.sendMail(_mailOps);
}
/**
* Send a text email to the admin.
* @param {string} subject - The subject of the email.
* @param {string} text - The body of the email.
* @param {string} cc - The CC email address.
*/
async function sendAdminNotification(subject, text, cc) {
return await sendTextMail({ subject, text }, '', cc);
}
/**
* Create an Email instance
* @param {*} emailOps { from, to }
*/
async function createEmail(emailOps, templateType = TemplateType.PUG) {
let _ops = Object.assign(
{
message: {
from: emailOps && emailOps.from || 'AgMission '
}
},
defaultMailOps);
if (templateType === 'hbs') {
_ops = {
..._ops, views: {
options: {
extension: 'hbs'
},
engineSource: { requires: { handlebars: hbs } }
},
/* For relative assets or ones located within the same server */
// juice: true,
// juiceResources: {
// webResources: {
// images: true,
// // relativeTo: path.join(process.cwd(), '/emails/assets'),
// }
// }
}
}
if (emailOps && emailOps.to) {
_ops.message.to = emailOps.to;
}
const email = new Email(_ops);
return email;
}
async function sendHandlebarsEmail(template, locals, to, from = null) {
if (env.NO_EMAIL_MODE) {
debug('Email sending is disabled in the current mode.');
return {
success: false,
messageId: null,
error: 'Email sending is disabled in the current mode'
};
}
assert(template, AppInputError.create(Errors.TEMPLATE_NOT_FOUND));
assert(to, AppInputError.create(Errors.TO_NOT_FOUND));
const email = await createEmail({ locale: 'en', to: to }, TemplateType.HANDLEBARS);
try {
const result = await email.send({
template: template,
locals: {
..._toLocale(locals)
}
});
// Debug the full result structure to find where messageId is located
// debug('Email send result structure:', JSON.stringify(result, null, 2));
// Check if the email was accepted by the mail server
if (result) {
let messageId = null;
// Try different possible locations for messageId
if (result.originalMessage && result.originalMessage.messageId) {
messageId = result.originalMessage.messageId;
} else if (result.messageId) {
messageId = result.messageId;
} else if (result.envelope && result.envelope.messageId) {
messageId = result.envelope.messageId;
}
// debug(`Email sent to ${to} successfully. MessageId: ${messageId || 'undefined'}`);
return {
success: true,
messageId: messageId,
result: result // Include the full result for debugging if needed
};
} else {
debug(`Email to ${to} has unknown status`);
return { success: false, error: 'Unknown email status' };
}
} catch (error) {
debug(`Failed to send email to ${to}: ${error.message}`);
// Capture specific nodemailer errors
const errorInfo = {
success: false,
error: error.message,
code: error.code,
responseCode: error.responseCode,
command: error.command
};
// Log detailed error for debugging
env.LOG_ALL_ERRORS && debug('Email sending error details:', errorInfo);
return errorInfo;
}
}
/**
* Validate email sending by sending a test email
* @param {string} toEmail - The email to test delivery to
* @returns {Promise