'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'), branding = require('./branding.js'), constants = require('./constants.js'), dashboard = require('./dashboard.js'), debug = require('debug')('box:mailer'), ejs = require('ejs'), mailServer = require('./mailserver.js'), nodemailer = require('nodemailer'), path = require('path'), safe = require('safetydance'), translation = require('./translation.js'), 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 branding.getCloudronName(); const { domain:dashboardDomain } = await dashboard.getLocation(); return { cloudronName, notificationFrom: `"${cloudronName}" `, supportEmail: 'support@cloudron.io' }; } async function sendMail(mailOptions) { assert.strictEqual(typeof mailOptions, 'object'); if (constants.TEST) { exports._mailQueue.push(mailOptions); return; } const { domain:dashboardDomain } = await dashboard.getLocation(); const data = await mailServer.getMailAuth(); const transport = nodemailer.createTransport({ host: data.ip, port: data.port, auth: { user: mailOptions.authUser || `no-reply@${dashboardDomain}`, pass: data.relayToken } }); const transportSendMail = util.promisify(transport.sendMail.bind(transport)); 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, email, inviteLink) { assert.strictEqual(typeof user, 'object'); assert.strictEqual(typeof invitor, 'object'); assert.strictEqual(typeof email, 'string'); assert.strictEqual(typeof inviteLink, 'string'); const mailConfig = await getMailConfig(); const translationAssets = await translation.getTranslations(); const { fqdn:dashboardFqdn } = await dashboard.getLocation(); const templateData = { user: user.displayName || user.username || user.email, webadminUrl: `https://${dashboardFqdn}`, inviteLink: inviteLink, invitor: invitor ? invitor.email : null, cloudronName: mailConfig.cloudronName, cloudronAvatarUrl: `https://${dashboardFqdn}/api/v1/cloudron/avatar` }; const mailOptions = { from: mailConfig.notificationFrom, to: email, 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 { fqdn:dashboardFqdn } = await dashboard.getLocation(); const templateData = { user: user.displayName || user.username || user.email, ip, userAgent: userAgent || 'unknown', country, city, cloudronName: mailConfig.cloudronName, cloudronAvatarUrl: `https://${dashboardFqdn}/api/v1/cloudron/avatar` }; const mailOptions = { from: mailConfig.notificationFrom, to: user.email, 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, email, resetLink) { assert.strictEqual(typeof user, 'object'); assert.strictEqual(typeof email, 'string'); assert.strictEqual(typeof resetLink, 'string'); const mailConfig = await getMailConfig(); const translationAssets = await translation.getTranslations(); const { fqdn:dashboardFqdn } = await dashboard.getLocation(); const templateData = { user: user.displayName || user.username || user.email, resetLink: resetLink, cloudronName: mailConfig.cloudronName, cloudronAvatarUrl: `https://${dashboardFqdn}/api/v1/cloudron/avatar` }; const mailOptions = { from: mailConfig.notificationFrom, to: email, 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) { assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof email, 'string'); const mailConfig = await getMailConfig(); const mailOptions = { authUser: `no-reply@${domain}`, from: `"${mailConfig.cloudronName}" `, to: email, subject: `[${mailConfig.cloudronName}] Test Email`, text: render('test.ejs', { cloudronName: mailConfig.cloudronName, format: 'text'}) }; await sendMail(mailOptions); }