previous mailer code has no callback and thus no way to pass back errors. now with asyncification it passes back the error
230 lines
7.8 KiB
JavaScript
230 lines
7.8 KiB
JavaScript
'use strict';
|
|
|
|
exports = module.exports = {
|
|
passwordReset,
|
|
|
|
sendInvite,
|
|
sendNewLoginLocation,
|
|
|
|
backupFailed,
|
|
|
|
certificateRenewalError,
|
|
|
|
sendTestMail,
|
|
|
|
_mailQueue: [] // accumulate mails in test mode
|
|
};
|
|
|
|
const assert = require('assert'),
|
|
BoxError = require('./boxerror.js'),
|
|
debug = require('debug')('box:mailer'),
|
|
ejs = require('ejs'),
|
|
mail = require('./mail.js'),
|
|
nodemailer = require('nodemailer'),
|
|
path = require('path'),
|
|
safe = require('safetydance'),
|
|
settings = require('./settings.js'),
|
|
translation = require('./translation.js'),
|
|
smtpTransport = require('nodemailer-smtp-transport'),
|
|
util = require('util');
|
|
|
|
const MAIL_TEMPLATES_DIR = path.join(__dirname, 'mail_templates');
|
|
|
|
// This will collect the most common details required for notification emails
|
|
async function getMailConfig() {
|
|
const cloudronName = await settings.getCloudronName();
|
|
const supportConfig = await settings.getSupportConfig();
|
|
|
|
return {
|
|
cloudronName,
|
|
notificationFrom: `"${cloudronName}" <no-reply@${settings.dashboardDomain()}>`,
|
|
supportEmail: supportConfig.email
|
|
};
|
|
}
|
|
|
|
async function sendMail(mailOptions) {
|
|
assert.strictEqual(typeof mailOptions, 'object');
|
|
|
|
if (process.env.BOX_ENV === 'test') {
|
|
exports._mailQueue.push(mailOptions);
|
|
return;
|
|
}
|
|
|
|
const data = await mail.getMailAuth();
|
|
|
|
const transport = nodemailer.createTransport(smtpTransport({
|
|
host: data.ip,
|
|
port: data.port,
|
|
auth: {
|
|
user: mailOptions.authUser || `no-reply@${settings.dashboardDomain()}`,
|
|
pass: data.relayToken
|
|
}
|
|
}));
|
|
|
|
const transportSendMail = util.promisify(transport.sendMail);
|
|
const [error] = await safe(transportSendMail(mailOptions));
|
|
if (error) throw new BoxError(BoxError.EXTERNAL_ERROR, error);
|
|
debug(`Email "${mailOptions.subject}" sent to ${mailOptions.to}`);
|
|
}
|
|
|
|
function render(templateFile, params, translationAssets) {
|
|
assert.strictEqual(typeof templateFile, 'string');
|
|
assert.strictEqual(typeof params, 'object');
|
|
|
|
let content = null;
|
|
let raw = safe.fs.readFileSync(path.join(MAIL_TEMPLATES_DIR, templateFile), 'utf8');
|
|
if (raw === null) {
|
|
debug(`Error loading ${templateFile}`);
|
|
return '';
|
|
}
|
|
|
|
if (typeof translationAssets === 'object') raw = translation.translate(raw, translationAssets.translations || {}, translationAssets.fallback || {});
|
|
|
|
try {
|
|
content = ejs.render(raw, params);
|
|
} catch (e) {
|
|
debug(`Error rendering ${templateFile}`, e);
|
|
}
|
|
|
|
return content;
|
|
}
|
|
|
|
async function sendInvite(user, invitor, inviteLink) {
|
|
assert.strictEqual(typeof user, 'object');
|
|
assert.strictEqual(typeof invitor, 'object');
|
|
assert.strictEqual(typeof inviteLink, 'string');
|
|
|
|
const mailConfig = await getMailConfig();
|
|
const translationAssets = await translation.getTranslations();
|
|
|
|
const templateData = {
|
|
user: user.displayName || user.username || user.email,
|
|
webadminUrl: settings.dashboardOrigin(),
|
|
inviteLink: inviteLink,
|
|
invitor: invitor ? invitor.email : null,
|
|
cloudronName: mailConfig.cloudronName,
|
|
cloudronAvatarUrl: settings.dashboardOrigin() + '/api/v1/cloudron/avatar'
|
|
};
|
|
|
|
const mailOptions = {
|
|
from: mailConfig.notificationFrom,
|
|
to: user.fallbackEmail,
|
|
subject: ejs.render(translation.translate('{{ welcomeEmail.subject }}', translationAssets.translations || {}, translationAssets.fallback || {}), { cloudron: mailConfig.cloudronName }),
|
|
text: render('welcome_user-text.ejs', templateData, translationAssets),
|
|
html: render('welcome_user-html.ejs', templateData, translationAssets)
|
|
};
|
|
|
|
await sendMail(mailOptions);
|
|
}
|
|
|
|
async function sendNewLoginLocation(user, loginLocation) {
|
|
assert.strictEqual(typeof user, 'object');
|
|
assert.strictEqual(typeof loginLocation, 'object');
|
|
|
|
const { ip, userAgent, country, city } = loginLocation;
|
|
|
|
assert.strictEqual(typeof ip, 'string');
|
|
assert.strictEqual(typeof userAgent, 'string');
|
|
assert.strictEqual(typeof country, 'string');
|
|
assert.strictEqual(typeof city, 'string');
|
|
|
|
const mailConfig = await getMailConfig();
|
|
const translationAssets = await translation.getTranslations();
|
|
|
|
const templateData = {
|
|
user: user.displayName || user.username || user.email,
|
|
ip,
|
|
userAgent: userAgent || 'unknown',
|
|
country,
|
|
city,
|
|
cloudronName: mailConfig.cloudronName,
|
|
cloudronAvatarUrl: settings.dashboardOrigin() + '/api/v1/cloudron/avatar'
|
|
};
|
|
|
|
const mailOptions = {
|
|
from: mailConfig.notificationFrom,
|
|
to: user.fallbackEmail,
|
|
subject: ejs.render(translation.translate('{{ newLoginEmail.subject }}', translationAssets.translations || {}, translationAssets.fallback || {}), { cloudron: mailConfig.cloudronName }),
|
|
text: render('new_login_location-text.ejs', templateData, translationAssets),
|
|
html: render('new_login_location-html.ejs', templateData, translationAssets)
|
|
};
|
|
|
|
await sendMail(mailOptions);
|
|
}
|
|
|
|
async function passwordReset(user) {
|
|
assert.strictEqual(typeof user, 'object');
|
|
|
|
const mailConfig = await getMailConfig();
|
|
const translationAssets = await translation.getTranslations();
|
|
|
|
const templateData = {
|
|
user: user.displayName || user.username || user.email,
|
|
resetLink: `${settings.dashboardOrigin()}/login.html?resetToken=${user.resetToken}`,
|
|
cloudronName: mailConfig.cloudronName,
|
|
cloudronAvatarUrl: settings.dashboardOrigin() + '/api/v1/cloudron/avatar'
|
|
};
|
|
|
|
const mailOptions = {
|
|
from: mailConfig.notificationFrom,
|
|
to: user.fallbackEmail,
|
|
subject: ejs.render(translation.translate('{{ passwordResetEmail.subject }}', translationAssets.translations || {}, translationAssets.fallback || {}), { cloudron: mailConfig.cloudronName }),
|
|
text: render('password_reset-text.ejs', templateData, translationAssets),
|
|
html: render('password_reset-html.ejs', templateData, translationAssets)
|
|
};
|
|
|
|
await sendMail(mailOptions);
|
|
}
|
|
|
|
async function backupFailed(mailTo, errorMessage, logUrl) {
|
|
assert.strictEqual(typeof mailTo, 'string');
|
|
assert.strictEqual(typeof errorMessage, 'string');
|
|
assert.strictEqual(typeof logUrl, 'string');
|
|
|
|
const mailConfig = await getMailConfig();
|
|
|
|
const mailOptions = {
|
|
from: mailConfig.notificationFrom,
|
|
to: mailTo,
|
|
subject: `[${mailConfig.cloudronName}] Failed to backup`,
|
|
text: render('backup_failed.ejs', { cloudronName: mailConfig.cloudronName, message: errorMessage, logUrl, format: 'text' })
|
|
};
|
|
|
|
await sendMail(mailOptions);
|
|
}
|
|
|
|
async function certificateRenewalError(mailTo, domain, message) {
|
|
assert.strictEqual(typeof mailTo, 'string');
|
|
assert.strictEqual(typeof domain, 'string');
|
|
assert.strictEqual(typeof message, 'string');
|
|
|
|
const mailConfig = await getMailConfig();
|
|
|
|
const mailOptions = {
|
|
from: mailConfig.notificationFrom,
|
|
to: mailTo,
|
|
subject: `[${mailConfig.cloudronName}] Certificate renewal error`,
|
|
text: render('certificate_renewal_error.ejs', { domain: domain, message: message, format: 'text' })
|
|
};
|
|
|
|
await sendMail(mailOptions);
|
|
}
|
|
|
|
async function sendTestMail(domain, email, callback) {
|
|
assert.strictEqual(typeof domain, 'string');
|
|
assert.strictEqual(typeof email, 'string');
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
const mailConfig = await getMailConfig();
|
|
|
|
const mailOptions = {
|
|
authUser: `no-reply@${domain}`,
|
|
from: `"${mailConfig.cloudronName}" <no-reply@${domain}>`,
|
|
to: email,
|
|
subject: `[${mailConfig.cloudronName}] Test Email`,
|
|
text: render('test.ejs', { cloudronName: mailConfig.cloudronName, format: 'text'})
|
|
};
|
|
|
|
await sendMail(mailOptions, callback);
|
|
}
|