import assert from 'node:assert'; import BoxError from './boxerror.js'; import branding from './branding.js'; import constants from './constants.js'; import dashboard from './dashboard.js'; import debugModule from 'debug'; import ejs from 'ejs'; import mailServer from './mailserver.js'; import nodemailer from 'nodemailer'; import path from 'node:path'; import safe from 'safetydance'; import translations from './translations.js'; const debug = debugModule('box:mailer'); const _mailQueue = []; // accumulate mails in test mode; function clearMailQueue() { _mailQueue.length = 0; } const MAIL_TEMPLATES_DIR = path.join(import.meta.dirname, 'mail_templates'); // This will collect the most common details required for notification emails async function getMailConfig() { const cloudronName = await branding.getCloudronName(); const { fqdn:dashboardFqdn, domain:dashboardDomain } = await dashboard.getLocation(); return { cloudronName, notificationFrom: `"${cloudronName}" `, supportEmail: 'support@cloudron.io', dashboardFqdn, notificationsUrl: `https://${dashboardFqdn}/cloudron/#/notifications` }; } async function sendMail(mailOptions) { assert.strictEqual(typeof mailOptions, 'object'); if (constants.TEST) { _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 }, connectionTimeout: 3000, greetingTimeout: 3000, socketTimeout: 3000, logger: false, debug: false // set to true for smtp logs }); const [error] = await safe(transport.sendMail(mailOptions)); if (error) throw new BoxError(BoxError.EXTERNAL_ERROR, `Error sending Email "${mailOptions.subject}" to ${mailOptions.to}: ${error.message}`); 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 = translations.translate(raw, translationAssets); 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 translations.getTranslations(); const templateData = { user: user.displayName || user.username || user.email, webadminUrl: `https://${mailConfig.dashboardFqdn}`, inviteLink: inviteLink, invitor: invitor ? invitor.email : null, cloudronName: mailConfig.cloudronName, cloudronAvatarUrl: `https://${mailConfig.dashboardFqdn}/api/v1/cloudron/avatar` }; const mailOptions = { from: mailConfig.notificationFrom, to: email, subject: ejs.render(translations.translate('{{ welcomeEmail.subject }}', translationAssets), { 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 translations.getTranslations(); const templateData = { user: user.displayName || user.username || user.email, ip, userAgent: userAgent || 'unknown', country, city, cloudronName: mailConfig.cloudronName, cloudronAvatarUrl: `https://${mailConfig.dashboardFqdn}/api/v1/cloudron/avatar` }; const mailOptions = { from: mailConfig.notificationFrom, to: user.email, subject: ejs.render(translations.translate('{{ newLoginEmail.subject }}', translationAssets), { 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 translations.getTranslations(); const templateData = { user: user.displayName || user.username || user.email, resetLink: resetLink, cloudronName: mailConfig.cloudronName, cloudronAvatarUrl: `https://${mailConfig.dashboardFqdn}/api/v1/cloudron/avatar` }; const mailOptions = { from: mailConfig.notificationFrom, to: email, subject: ejs.render(translations.translate('{{ passwordResetEmail.subject }}', translationAssets), { cloudron: mailConfig.cloudronName }), text: render('password_reset-text.ejs', templateData, translationAssets), html: render('password_reset-html.ejs', templateData, translationAssets) }; await sendMail(mailOptions); } async function appDown(mailTo, app) { assert.strictEqual(typeof mailTo, 'string'); assert.strictEqual(typeof app, 'object'); const mailConfig = await getMailConfig(); const mailOptions = { from: mailConfig.notificationFrom, to: mailTo, subject: `[${mailConfig.cloudronName}] App ${app.fqdn} is down`, text: render('app_down-text.ejs', { title: app.manifest.title, appFqdn: app.fqdn, notificationsUrl: mailConfig.notificationsUrl }) }; await sendMail(mailOptions); } async function appUp(mailTo, app) { assert.strictEqual(typeof mailTo, 'string'); assert.strictEqual(typeof app, 'object'); const mailConfig = await getMailConfig(); const mailOptions = { from: mailConfig.notificationFrom, to: mailTo, subject: `[${mailConfig.cloudronName}] App ${app.fqdn} is back online`, text: render('app_up-text.ejs', { title: app.manifest.title, appFqdn: app.fqdn, notificationsUrl: mailConfig.notificationsUrl }) }; await sendMail(mailOptions); } async function oomEvent(mailTo, containerId, app, addon, event) { assert.strictEqual(typeof mailTo, 'string'); assert.strictEqual(typeof containerId, 'string'); assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof addon, 'object'); assert.strictEqual(typeof event, 'object'); const mailConfig = await getMailConfig(); const templateData = { webadminUrl: `https://${mailConfig.dashboardFqdn}`, cloudronName: mailConfig.cloudronName, app, addon, event: JSON.stringify(event), notificationsUrl: mailConfig.notificationsUrl }; const mailOptions = { from: mailConfig.notificationFrom, to: mailTo, subject: `[${mailConfig.cloudronName}] ${app ? app.fqdn : addon.name} was restarted (OOM)`, text: render('oom_event-text.ejs', templateData) }; 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-text.ejs', { cloudronName: mailConfig.cloudronName, message: errorMessage, logUrl, notificationsUrl: mailConfig.notificationsUrl }) }; await sendMail(mailOptions); } async function rebootRequired(mailTo) { assert.strictEqual(typeof mailTo, 'string'); const mailConfig = await getMailConfig(); const mailOptions = { from: mailConfig.notificationFrom, to: mailTo, subject: `[${mailConfig.cloudronName}] Reboot required for security updates`, text: render('reboot_required-text.ejs', { webadminUrl: `https://${mailConfig.dashboardFqdn}`, notificationsUrl: mailConfig.notificationsUrl }) }; await sendMail(mailOptions); } async function lowDiskSpace(mailTo, message) { assert.strictEqual(typeof mailTo, 'string'); assert.strictEqual(typeof message, 'string'); const mailConfig = await getMailConfig(); const mailOptions = { from: mailConfig.notificationFrom, to: mailTo, subject: `[${mailConfig.cloudronName}] Server is running low on disk space`, text: render('low_disk_space-text.ejs', { message, notificationsUrl: mailConfig.notificationsUrl }) }; await sendMail(mailOptions); } async function boxUpdateError(mailTo, message) { assert.strictEqual(typeof mailTo, 'string'); assert.strictEqual(typeof message, 'string'); const mailConfig = await getMailConfig(); const mailOptions = { from: mailConfig.notificationFrom, to: mailTo, subject: `[${mailConfig.cloudronName}] Cloudron update error`, text: render('box_update_error-text.ejs', { message, notificationsUrl: mailConfig.notificationsUrl }) }; 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-text.ejs', { domain, message, notificationsUrl: mailConfig.notificationsUrl }) }; 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-text.ejs', { cloudronName: mailConfig.cloudronName}), html: render('test-html.ejs', { cloudronName: mailConfig.cloudronName }) }; await sendMail(mailOptions); } export default { passwordReset, sendInvite, sendNewLoginLocation, backupFailed, certificateRenewalError, appDown, appUp, oomEvent, rebootRequired, boxUpdateError, lowDiskSpace, sendTestMail, _mailQueue, clearMailQueue, };