diff --git a/CHANGES b/CHANGES index 41b21fab1..5e13babba 100644 --- a/CHANGES +++ b/CHANGES @@ -3207,5 +3207,5 @@ [9.1.6] * apps: fix wrong disabled state for devices config -* notifications: send email when manual platform update required +* notifications: send email when manual platform and app update required diff --git a/src/mail_templates/app_manual_update_required-text.ejs b/src/mail_templates/app_manual_update_required-text.ejs new file mode 100644 index 000000000..c65b4a9fe --- /dev/null +++ b/src/mail_templates/app_manual_update_required-text.ejs @@ -0,0 +1,15 @@ +Dear <%= cloudronName %> Admin, + +The following app(s) require manual update: + +<% for (const app of apps) { -%> +* <%= app.title %> at <%= app.fqdn %> -> v<%= app.version %> +<% } -%> + +Go to the update section of the app to update. + +Powered by https://cloudron.io + +Don't want such mails? Change your notification preferences at <%= notificationsUrl %> + +Sent at: <%= new Date().toUTCString() %> diff --git a/src/mailer.js b/src/mailer.js index f1877980c..e1429aecc 100644 --- a/src/mailer.js +++ b/src/mailer.js @@ -339,6 +339,22 @@ async function boxManualUpdateRequired(mailTo, version, changelog) { await sendMail(mailOptions); } +async function appManualUpdateRequired(mailTo, apps) { + assert.strictEqual(typeof mailTo, 'string'); + assert(Array.isArray(apps)); + + const mailConfig = await getMailConfig(); + + const mailOptions = { + from: mailConfig.notificationFrom, + to: mailTo, + subject: `[${mailConfig.cloudronName}] ${apps.length} app(s) require manual update`, + text: render('app_manual_update_required-text.ejs', { cloudronName: mailConfig.cloudronName, apps, notificationsUrl: mailConfig.notificationsUrl }) + }; + + await sendMail(mailOptions); +} + async function certificateRenewalError(mailTo, domain, message) { assert.strictEqual(typeof mailTo, 'string'); assert.strictEqual(typeof domain, 'string'); @@ -386,6 +402,7 @@ export default { oomEvent, rebootRequired, boxManualUpdateRequired, + appManualUpdateRequired, boxUpdateError, lowDiskSpace, sendTestMail, diff --git a/src/notifications.js b/src/notifications.js index 790f6b8c0..79fa281e2 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -350,6 +350,33 @@ async function pin(type, title, message, options) { return result.id; } +async function manualAppUpdate(appsNeedingUpdate) { + assert(Array.isArray(appsNeedingUpdate)); + + const appsNeedingEmail = []; + + for (const app of appsNeedingUpdate) { + const message = `Changelog:\n${app.updateInfo.manifest.changelog}\n`; + const existing = await getByType(TYPE_MANUAL_APP_UPDATE_NEEDED, app.id); + + if (!existing || (existing.acknowledged && existing.message !== message)) { + appsNeedingEmail.push({ title: app.manifest.title, fqdn: app.fqdn, version: app.updateInfo.manifest.version }); + } + + await pin(TYPE_MANUAL_APP_UPDATE_NEEDED, `${app.manifest.title} at ${app.fqdn} requires manual update to version ${app.updateInfo.manifest.version}`, + message, { context: app.id }); + } + + if (appsNeedingEmail.length === 0) return; + + const admins = await users.getAdmins(); + for (const admin of admins) { + if (admin.notificationConfig.includes(TYPE_MANUAL_UPDATE_REQUIRED)) { + await safe(mailer.appManualUpdateRequired(admin.email, appsNeedingEmail), { debug: log }); + } + } +} + async function unpin(type, options) { assert.strictEqual(typeof type, 'string'); // TYPE_ assert.strictEqual(typeof options, 'object'); @@ -432,5 +459,6 @@ export default { TYPE_DOMAIN_CONFIG_CHECK_FAILED, pin, unpin, + manualAppUpdate, _add: add, }; diff --git a/src/updater.js b/src/updater.js index 5c83f0ed5..fd3f1c90a 100644 --- a/src/updater.js +++ b/src/updater.js @@ -359,18 +359,19 @@ async function raiseNotifications() { } const result = await apps.list(); + const manualUpdateApps = []; + for (const app of result) { if (!app.updateInfo) continue; - // currently, we do not raise notifications when auto-update is disabled. separate notifications appears spammy when having many apps - // in the future, we can maybe aggregate? if (!app.updateInfo.isAutoUpdatable) { log(`autoUpdate: ${app.fqdn} cannot be autoupdated. skipping`); - await notifications.pin(notifications.TYPE_MANUAL_APP_UPDATE_NEEDED, `${app.manifest.title} at ${app.fqdn} requires manual update to version ${app.updateInfo.manifest.version}`, - `Changelog:\n${app.updateInfo.manifest.changelog}\n`, { context: app.id }); + manualUpdateApps.push(app); continue; } } + + await safe(notifications.manualAppUpdate(manualUpdateApps), { debug: log }); } async function checkForUpdates(options) {