'use strict'; exports = module.exports = { passwordReset, boxUpdateAvailable, appUpdatesAvailable, sendInvite, appUp, appDied, appUpdated, oomEvent, backupFailed, certificateRenewalError, boxUpdateError, sendTestMail, _mailQueue: [] // accumulate mails in test mode }; var 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'), showdown = require('showdown'), translation = require('./translation.js'), smtpTransport = require('nodemailer-smtp-transport'), util = require('util'); var NOOP_CALLBACK = function (error) { if (error) debug(error); }; var MAIL_TEMPLATES_DIR = path.join(__dirname, 'mail_templates'); // This will collect the most common details required for notification emails function getMailConfig(callback) { assert.strictEqual(typeof callback, 'function'); settings.getCloudronName(function (error, cloudronName) { if (error) debug('Error getting cloudron name: ', error); settings.getSupportConfig(function (error, supportConfig) { if (error) debug('Error getting support config: ', error); callback(null, { cloudronName: cloudronName || '', notificationFrom: `"${cloudronName}" `, supportEmail: supportConfig.email }); }); }); } function sendMail(mailOptions, callback) { assert.strictEqual(typeof mailOptions, 'object'); callback = callback || NOOP_CALLBACK; if (process.env.BOX_ENV === 'test') { exports._mailQueue.push(mailOptions); return callback(); } mail.getMailAuth(function (error, data) { if (error) return callback(error); var transport = nodemailer.createTransport(smtpTransport({ host: data.ip, port: data.port, auth: { user: mailOptions.authUser || `no-reply@${settings.adminDomain()}`, pass: data.relayToken } })); transport.sendMail(mailOptions, function (error) { if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error)); debug(`Email "${mailOptions.subject}" sent to ${mailOptions.to}`); callback(null); }); }); } function render(templateFile, params, translationAssets) { assert.strictEqual(typeof templateFile, 'string'); assert.strictEqual(typeof params, 'object'); var content = null; var 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; } function sendInvite(user, invitor, inviteLink) { assert.strictEqual(typeof user, 'object'); assert.strictEqual(typeof invitor, 'object'); assert.strictEqual(typeof inviteLink, 'string'); debug('Sending invite mail'); getMailConfig(function (error, mailConfig) { if (error) return debug('Error getting mail details:', error); translation.getTranslations(function (error, translationAssets) { if (error) return debug('Error getting translations:', error); var templateData = { user: user.displayName || user.username || user.email, webadminUrl: settings.adminOrigin(), inviteLink: inviteLink, invitor: invitor ? invitor.email : null, cloudronName: mailConfig.cloudronName, cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar' }; var 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) }; sendMail(mailOptions); }); }); } function passwordReset(user) { assert.strictEqual(typeof user, 'object'); debug('Sending mail for password reset for user %s.', user.email, user.id); getMailConfig(function (error, mailConfig) { if (error) return debug('Error getting mail details:', error); translation.getTranslations(function (error, translationAssets) { if (error) return debug('Error getting translations:', error); var templateData = { user: user.displayName || user.username || user.email, resetLink: `${settings.adminOrigin()}/login.html?resetToken=${user.resetToken}`, cloudronName: mailConfig.cloudronName, cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar' }; var 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) }; sendMail(mailOptions); }); }); } function appUp(mailTo, app) { assert.strictEqual(typeof mailTo, 'string'); assert.strictEqual(typeof app, 'object'); debug('Sending mail for app %s @ %s up', app.id, app.fqdn); getMailConfig(function (error, mailConfig) { if (error) return debug('Error getting mail details:', error); var mailOptions = { from: mailConfig.notificationFrom, to: mailTo, subject: util.format('[%s] App %s is back online', mailConfig.cloudronName, app.fqdn), text: render('app_up.ejs', { title: app.manifest.title, appFqdn: app.fqdn, format: 'text' }) }; sendMail(mailOptions); }); } function appDied(mailTo, app) { assert.strictEqual(typeof mailTo, 'string'); assert.strictEqual(typeof app, 'object'); debug('Sending mail for app %s @ %s died', app.id, app.fqdn); getMailConfig(function (error, mailConfig) { if (error) return debug('Error getting mail details:', error); var mailOptions = { from: mailConfig.notificationFrom, to: mailTo, subject: util.format('[%s] App %s is down', mailConfig.cloudronName, app.fqdn), text: render('app_down.ejs', { title: app.manifest.title, appFqdn: app.fqdn, supportEmail: mailConfig.supportEmail, format: 'text' }) }; sendMail(mailOptions); }); } function appUpdated(mailTo, app, callback) { assert.strictEqual(typeof mailTo, 'string'); assert.strictEqual(typeof app, 'object'); callback = callback || NOOP_CALLBACK; debug('Sending mail for app %s @ %s updated', app.id, app.fqdn); getMailConfig(function (error, mailConfig) { if (error) return debug('Error getting mail details:', error); var converter = new showdown.Converter(); var templateData = { title: app.manifest.title, appFqdn: app.fqdn, version: app.manifest.version, changelog: app.manifest.changelog, changelogHTML: converter.makeHtml(app.manifest.changelog), cloudronName: mailConfig.cloudronName, cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar' }; var templateDataText = JSON.parse(JSON.stringify(templateData)); templateDataText.format = 'text'; var templateDataHTML = JSON.parse(JSON.stringify(templateData)); templateDataHTML.format = 'html'; var mailOptions = { from: mailConfig.notificationFrom, to: mailTo, subject: `[${mailConfig.cloudronName}] App ${app.fqdn} was updated`, text: render('app_updated.ejs', templateDataText), html: render('app_updated.ejs', templateDataHTML) }; sendMail(mailOptions, callback); }); } function boxUpdateAvailable(mailTo, updateInfo, callback) { assert.strictEqual(typeof mailTo, 'string'); assert.strictEqual(typeof updateInfo, 'object'); assert.strictEqual(typeof callback, 'function'); getMailConfig(function (error, mailConfig) { if (error) return debug('Error getting mail details:', error); var converter = new showdown.Converter(); var templateData = { webadminUrl: settings.adminOrigin(), newBoxVersion: updateInfo.version, changelog: updateInfo.changelog, changelogHTML: updateInfo.changelog.map(function (e) { return converter.makeHtml(e); }), cloudronName: mailConfig.cloudronName, cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar' }; var templateDataText = JSON.parse(JSON.stringify(templateData)); templateDataText.format = 'text'; var templateDataHTML = JSON.parse(JSON.stringify(templateData)); templateDataHTML.format = 'html'; var mailOptions = { from: mailConfig.notificationFrom, to: mailTo, subject: 'Cloudron update available', text: render('box_update_available.ejs', templateDataText), html: render('box_update_available.ejs', templateDataHTML) }; sendMail(mailOptions, callback); }); } function appUpdatesAvailable(mailTo, apps, callback) { assert.strictEqual(typeof mailTo, 'string'); assert.strictEqual(typeof apps, 'object'); assert.strictEqual(typeof callback, 'function'); getMailConfig(function (error, mailConfig) { if (error) return debug('Error getting mail details:', error); var converter = new showdown.Converter(); apps.forEach(function (app) { app.changelogHTML = converter.makeHtml(app.updateInfo.manifest.changelog); }); var templateData = { webadminUrl: settings.adminOrigin(), apps: apps, cloudronName: mailConfig.cloudronName, cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar' }; var templateDataText = JSON.parse(JSON.stringify(templateData)); templateDataText.format = 'text'; var templateDataHTML = JSON.parse(JSON.stringify(templateData)); templateDataHTML.format = 'html'; var mailOptions = { from: mailConfig.notificationFrom, to: mailTo, subject: 'App updates available', text: render('app_updates_available.ejs', templateDataText), html: render('app_updates_available.ejs', templateDataHTML) }; sendMail(mailOptions, callback); }); } function backupFailed(mailTo, errorMessage, logUrl) { assert.strictEqual(typeof mailTo, 'string'); getMailConfig(function (error, mailConfig) { if (error) return debug('Error getting mail details:', error); var mailOptions = { from: mailConfig.notificationFrom, to: mailTo, subject: util.format('[%s] Failed to backup', mailConfig.cloudronName), text: render('backup_failed.ejs', { cloudronName: mailConfig.cloudronName, message: errorMessage, logUrl: logUrl, format: 'text' }) }; sendMail(mailOptions); }); } function certificateRenewalError(mailTo, domain, message) { assert.strictEqual(typeof mailTo, 'string'); assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof message, 'string'); getMailConfig(function (error, mailConfig) { if (error) return debug('Error getting mail details:', error); var mailOptions = { from: mailConfig.notificationFrom, to: mailTo, subject: util.format('[%s] Certificate renewal error', domain), text: render('certificate_renewal_error.ejs', { domain: domain, message: message, format: 'text' }) }; sendMail(mailOptions); }); } function boxUpdateError(mailTo, message) { assert.strictEqual(typeof mailTo, 'string'); assert.strictEqual(typeof message, 'string'); getMailConfig(function (error, mailConfig) { if (error) return debug('Error getting mail details:', error); var mailOptions = { from: mailConfig.notificationFrom, to: mailTo, subject: util.format('[%s] Cloudron update error', mailConfig.cloudronName), text: render('box_update_error.ejs', { message: message, format: 'text' }) }; sendMail(mailOptions); }); } function oomEvent(mailTo, program, event) { assert.strictEqual(typeof mailTo, 'string'); assert.strictEqual(typeof program, 'string'); assert.strictEqual(typeof event, 'object'); getMailConfig(function (error, mailConfig) { if (error) return debug('Error getting mail details:', error); var mailOptions = { from: mailConfig.notificationFrom, to: mailTo, subject: util.format('[%s] %s was restarted (OOM)', mailConfig.cloudronName, program), text: render('oom_event.ejs', { cloudronName: mailConfig.cloudronName, program: program, event: JSON.stringify(event), format: 'text' }) }; sendMail(mailOptions); }); } function sendTestMail(domain, email, callback) { assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof email, 'string'); assert.strictEqual(typeof callback, 'function'); getMailConfig(function (error, mailConfig) { if (error) return debug('Error getting mail details:', error); var mailOptions = { authUser: `no-reply@${domain}`, from: `"${mailConfig.cloudronName}" `, to: email, subject: util.format('Test Email from %s', mailConfig.cloudronName), text: render('test.ejs', { cloudronName: mailConfig.cloudronName, format: 'text'}) }; sendMail(mailOptions, callback); }); }