'use strict'; exports = module.exports = { userAdded: userAdded, userRemoved: userRemoved, roleChanged: roleChanged, passwordReset: passwordReset, appUpdatesAvailable: appUpdatesAvailable, sendInvite: sendInvite, appUp: appUp, appDied: appDied, appUpdated: appUpdated, oomEvent: oomEvent, backupFailed: backupFailed, certificateRenewalError: certificateRenewalError, boxUpdateError: boxUpdateError, sendTestMail: 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'), 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) { assert.strictEqual(typeof templateFile, 'string'); assert.strictEqual(typeof params, 'object'); var content = null; try { content = ejs.render(safe.fs.readFileSync(path.join(MAIL_TEMPLATES_DIR, templateFile), 'utf8'), params); } catch (e) { debug(`Error rendering ${templateFile}`, e); } return content; } function mailUserEvent(mailTo, user, event) { assert.strictEqual(typeof mailTo, 'string'); assert.strictEqual(typeof user, 'object'); assert.strictEqual(typeof event, '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] %s %s', mailConfig.cloudronName, user.username || user.fallbackEmail || user.email, event), text: render('user_event.ejs', { user: user, event: event, format: 'text' }), }; sendMail(mailOptions); }); } 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); var templateData = { user: user, webadminUrl: settings.adminOrigin(), inviteLink: inviteLink, invitor: invitor, 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: user.fallbackEmail, subject: util.format('Welcome to %s', mailConfig.cloudronName), text: render('welcome_user.ejs', templateDataText), html: render('welcome_user.ejs', templateDataHTML) }; sendMail(mailOptions); }); } function userAdded(mailTo, user) { assert.strictEqual(typeof mailTo, 'string'); assert.strictEqual(typeof user, 'object'); debug(`userAdded: Sending mail for added users ${user.fallbackEmail} to ${mailTo}`); getMailConfig(function (error, mailConfig) { if (error) return debug('Error getting mail details:', error); var templateData = { user: user, 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: util.format('[%s] User %s added', mailConfig.cloudronName, user.fallbackEmail), text: render('user_added.ejs', templateDataText), html: render('user_added.ejs', templateDataHTML) }; sendMail(mailOptions); }); } function userRemoved(mailTo, user) { assert.strictEqual(typeof mailTo, 'string'); assert.strictEqual(typeof user, 'object'); debug('Sending mail for userRemoved.', user.id, user.username, user.email); mailUserEvent(mailTo, user, 'was removed'); } function roleChanged(mailTo, user) { assert.strictEqual(typeof mailTo, 'string'); assert.strictEqual(typeof user, 'object'); debug('Sending mail for roleChanged'); mailUserEvent(mailTo, user, `now has the role '${user.role}'`); } 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); var templateData = { user: user, resetLink: `${settings.adminOrigin()}/login.html?resetToken=${user.resetToken}`, 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: user.fallbackEmail, subject: util.format('[%s] Password Reset', mailConfig.cloudronName), text: render('password_reset.ejs', templateDataText), html: render('password_reset.ejs', templateDataHTML) }; 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 appUpdatesAvailable(mailTo, apps, hasSubscription, callback) { assert.strictEqual(typeof mailTo, 'string'); assert.strictEqual(typeof apps, 'object'); assert.strictEqual(typeof hasSubscription, 'boolean'); 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(), hasSubscription: hasSubscription, 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: `New app updates available for ${mailConfig.cloudronName}`, 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); }); }