mostly because code is being autogenerated by all the AI stuff using this prefix. it's also used in the stack trace.
352 lines
12 KiB
JavaScript
352 lines
12 KiB
JavaScript
'use strict';
|
|
|
|
exports = module.exports = {
|
|
passwordReset,
|
|
|
|
sendInvite,
|
|
sendNewLoginLocation,
|
|
|
|
backupFailed,
|
|
certificateRenewalError,
|
|
appDown,
|
|
appUp,
|
|
oomEvent,
|
|
rebootRequired,
|
|
boxUpdateError,
|
|
lowDiskSpace,
|
|
|
|
sendTestMail,
|
|
|
|
_mailQueue: [] // accumulate mails in test mode
|
|
};
|
|
|
|
const assert = require('node: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('node:path'),
|
|
safe = require('safetydance'),
|
|
translations = require('./translations.js');
|
|
|
|
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 { fqdn:dashboardFqdn, domain:dashboardDomain } = await dashboard.getLocation();
|
|
|
|
return {
|
|
cloudronName,
|
|
notificationFrom: `"${cloudronName}" <no-reply@${dashboardDomain}>`,
|
|
supportEmail: 'support@cloudron.io',
|
|
dashboardFqdn,
|
|
notificationsUrl: `https://${dashboardFqdn}/cloudron/#/notifications`
|
|
};
|
|
}
|
|
|
|
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
|
|
},
|
|
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}" <no-reply@${domain}>`,
|
|
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);
|
|
}
|