diff --git a/src/mail_templates/app_updates_available.ejs b/src/mail_templates/app_updates_available.ejs index 341ab1ecc..7e083017e 100644 --- a/src/mail_templates/app_updates_available.ejs +++ b/src/mail_templates/app_updates_available.ejs @@ -3,9 +3,9 @@ Dear Cloudron Admin, <% for (var i = 0; i < apps.length; i++) { -%> -A new version <%= apps[i].updateInfo.manifest.version %> of the app '<%= apps[i].app.manifest.title %>' installed at <%= apps[i].app.fqdn %> is available. +The app '<%= apps[i].app.manifest.title %>' installed at <%= apps[i].app.fqdn %> has an update available. -Changes: +<%= apps[i].app.manifest.title %> v<%= apps[i].updateInfo.manifest.version %> changes: <%= apps[i].updateInfo.manifest.changelog %> <% } -%> @@ -29,10 +29,10 @@ Sent at: <%= new Date().toUTCString() %>
<% for (var i = 0; i < apps.length; i++) { -%>

- A new version <%= apps[i].updateInfo.manifest.version %> of the app '<%= apps[i].app.manifest.title %>' installed at <%= apps[i].app.fqdn %> is available. + The app '<%= apps[i].app.manifest.title %>' installed at <%= apps[i].app.fqdn %> has an update available.

-
Changelog:
+
<%= apps[i].app.manifest.title %> v<%= apps[i].updateInfo.manifest.version %> changes:
<%- apps[i].changelogHTML %>
diff --git a/src/mail_templates/box_update_available.ejs b/src/mail_templates/box_update_available.ejs new file mode 100644 index 000000000..a69246fd6 --- /dev/null +++ b/src/mail_templates/box_update_available.ejs @@ -0,0 +1,45 @@ +<%if (format === 'text') { %> + +Dear <%= cloudronName %> Admin, + +Cloudron v<%= newBoxVersion %> is now available! + +Changes: +<% for (var i = 0; i < changelog.length; i++) { %> + * <%- changelog[i] %> +<% } %> + +Powered by https://cloudron.io + +Sent at: <%= new Date().toUTCString() %> + +<% } else { %> + +
+ + + +

Dear <%= cloudronName %> Admin,

+ +
+

+ Cloudron v<%= newBoxVersion %> is now available! +

+ +
Changes:
+
    + <% for (var i = 0; i < changelogHTML.length; i++) { %> +
  • <%- changelogHTML[i] %>
  • + <% } %> +
+ +
+
+ +
+ Powered by Cloudron. +
+ +
+ +<% } %> diff --git a/src/mailer.js b/src/mailer.js index 586933fef..2f1674db6 100644 --- a/src/mailer.js +++ b/src/mailer.js @@ -1,22 +1,23 @@ 'use strict'; exports = module.exports = { - passwordReset: passwordReset, - appUpdatesAvailable: appUpdatesAvailable, + passwordReset, + boxUpdateAvailable, + appUpdatesAvailable, - sendInvite: sendInvite, + sendInvite, - appUp: appUp, - appDied: appDied, - appUpdated: appUpdated, - oomEvent: oomEvent, + appUp, + appDied, + appUpdated, + oomEvent, - backupFailed: backupFailed, + backupFailed, - certificateRenewalError: certificateRenewalError, - boxUpdateError: boxUpdateError, + certificateRenewalError, + boxUpdateError, - sendTestMail: sendTestMail, + sendTestMail, _mailQueue: [] // accumulate mails in test mode }; @@ -256,6 +257,43 @@ function appUpdated(mailTo, app, callback) { }); } +function boxUpdateAvailable(mailTo, updateInfo, callback) { + assert.strictEqual(typeof mailTo, 'string'); + assert.strictEqual(typeof updateInfo, 'object'); + assert.strictEqual(typeof callback, 'function'); + + getMailConfig(function (error, mailConfig) { + if (error) return debug('Error getting mail details:', error); + + var converter = new showdown.Converter(); + + var templateData = { + webadminUrl: settings.adminOrigin(), + newBoxVersion: updateInfo.version, + changelog: updateInfo.changelog, + changelogHTML: updateInfo.changelog.map(function (e) { return converter.makeHtml(e); }), + 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: 'Cloudron update available', + text: render('box_update_available.ejs', templateDataText), + html: render('box_update_available.ejs', templateDataHTML) + }; + + sendMail(mailOptions, callback); + }); +} + function appUpdatesAvailable(mailTo, apps, callback) { assert.strictEqual(typeof mailTo, 'string'); assert.strictEqual(typeof apps, 'object'); @@ -285,7 +323,7 @@ function appUpdatesAvailable(mailTo, apps, callback) { var mailOptions = { from: mailConfig.notificationFrom, to: mailTo, - subject: `New app updates available for ${mailConfig.cloudronName}`, + subject: 'App updates available', text: render('app_updates_available.ejs', templateDataText), html: render('app_updates_available.ejs', templateDataHTML) }; diff --git a/src/notifications.js b/src/notifications.js index a7e614d15..adb992dd7 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -1,11 +1,14 @@ 'use strict'; exports = module.exports = { - get: get, - ack: ack, - getAllPaged: getAllPaged, + get, + ack, + getAllPaged, - onEvent: onEvent, + onEvent, + + appUpdatesAvailable, + boxUpdateAvailable, // NOTE: if you add an alert, be sure to add title below ALERT_BACKUP_CONFIG: 'backupConfig', @@ -20,11 +23,13 @@ exports = module.exports = { _add: add }; -let assert = require('assert'), +let apps = require('./apps.js'), + assert = require('assert'), async = require('async'), auditSource = require('./auditsource.js'), BoxError = require('./boxerror.js'), changelog = require('./changelog.js'), + constants = require('./constants.js'), debug = require('debug')('box:notifications'), eventlog = require('./eventlog.js'), mailer = require('./mailer.js'), @@ -217,6 +222,39 @@ function appUpdated(eventId, app, callback) { }, callback); } +function boxUpdateAvailable(updateInfo, callback) { + assert.strictEqual(typeof updateInfo, 'object'); + assert.strictEqual(typeof callback, 'function'); + + settings.getAutoupdatePattern(function (error, result) { + if (error) return callback(error); + + if (result !== constants.AUTOUPDATE_PATTERN_NEVER) return callback(); + + forEachAdmin({ skip: [] }, function (admin, done) { + mailer.boxUpdateAvailable(admin.email, updateInfo, done); + }, callback); + }); +} + +function appUpdatesAvailable(appUpdates, callback) { + assert.strictEqual(typeof appUpdates, 'object'); + assert.strictEqual(typeof callback, 'function'); + + settings.getAutoupdatePattern(function (error, result) { + if (error) return callback(error); + + // if we are auto updating, then just consider apps that cannot be auto updated + if (result !== constants.AUTOUPDATE_PATTERN_NEVER) appUpdates = appUpdates.filter(update => !apps.canAutoupdateApp(update.app, update.updateInfo)); + + if (appUpdates.length === 0) return callback(); + + forEachAdmin({ skip: [] }, function (admin, done) { + mailer.appUpdatesAvailable(admin.email, appUpdates, done); + }, callback); + }); +} + function boxUpdated(eventId, oldVersion, newVersion, callback) { assert.strictEqual(typeof eventId, 'string'); assert.strictEqual(typeof oldVersion, 'string'); diff --git a/src/test/updatechecker-test.js b/src/test/updatechecker-test.js index 49616c83b..9319d86ea 100644 --- a/src/test/updatechecker-test.js +++ b/src/test/updatechecker-test.js @@ -124,7 +124,7 @@ describe('updatechecker - box', function () { expect(updatechecker.getUpdateInfo().box.sourceTarballUrl).to.be('box.tar.gz'); expect(scope.isDone()).to.be.ok(); - checkMails(0, done); // it seems we stopped sending mails for box updates! + checkMails(1, done); }); }); diff --git a/src/updatechecker.js b/src/updatechecker.js index 72692c55f..de6213ae1 100644 --- a/src/updatechecker.js +++ b/src/updatechecker.js @@ -12,14 +12,10 @@ var apps = require('./apps.js'), appstore = require('./appstore.js'), assert = require('assert'), async = require('async'), - constants = require('./constants.js'), debug = require('debug')('box:updatechecker'), - mailer = require('./mailer.js'), notifications = require('./notifications.js'), paths = require('./paths.js'), - safe = require('safetydance'), - settings = require('./settings.js'), - users = require('./users.js'); + safe = require('safetydance'); function setUpdateInfo(state) { // appid -> update info { creationDate, manifest } @@ -44,55 +40,38 @@ function checkAppUpdates(options, callback) { let state = getUpdateInfo(); let newState = { }; // create new state so that old app ids are removed - settings.getAutoupdatePattern(function (error, result) { + var pendingNotifications = []; + + apps.getAll(function (error, result) { if (error) return callback(error); - const autoupdatesEnabled = (result !== constants.AUTOUPDATE_PATTERN_NEVER); - var notificationPending = []; + async.eachSeries(result, function (app, iteratorDone) { + if (app.appStoreId === '') return iteratorDone(); // appStoreId can be '' for dev apps - apps.getAll(function (error, result) { - if (error) return callback(error); + appstore.getAppUpdate(app, options, function (error, updateInfo) { + if (error) { + debug('Error getting app update info for %s', app.id, error); + return iteratorDone(); // continue to next + } - async.eachSeries(result, function (app, iteratorDone) { - if (app.appStoreId === '') return iteratorDone(); // appStoreId can be '' for dev apps + if (!updateInfo) return iteratorDone(); // skip if no next version is found - appstore.getAppUpdate(app, options, function (error, updateInfo) { - if (error) { - debug('Error getting app update info for %s', app.id, error); - return iteratorDone(); // continue to next - } + newState[app.id] = updateInfo; - if (!updateInfo) return iteratorDone(); // skip if no next version is found + if (safe.query(state[app.id], 'manifest.version') === updateInfo.manifest.version) { + debug(`Skipping app update notification of ${app.id} since user was already notified of ${updateInfo.manifest.version}`); + return iteratorDone(); + } - newState[app.id] = updateInfo; - - if (safe.query(state[app.id], 'manifest.version') === updateInfo.manifest.version) { - debug(`Skipping app update notification of ${app.id} since user was already notified of ${updateInfo.manifest.version}`); - return iteratorDone(); - } - - const canAutoupdateApp = apps.canAutoupdateApp(app, updateInfo); - if (autoupdatesEnabled && canAutoupdateApp) return iteratorDone(); - - debug(`Notifying of app update for ${app.id} from ${app.manifest.version} to ${updateInfo.manifest.version}`); - notificationPending.push({ app, updateInfo }); - iteratorDone(); - }); - }, function () { - if ('box' in state) newState.box = state.box; // preserve the latest box state information - setUpdateInfo(newState); - - if (notificationPending.length === 0) return callback(); - - users.getAdmins(function (error, admins) { - if (error) { - debug('checkAppUpdates: failed to get admins', error); - return callback(); - } - - async.eachSeries(admins, (admin, done) => mailer.appUpdatesAvailable(admin.email, notificationPending, done), callback); - }); + pendingNotifications.push({ app, updateInfo }); + iteratorDone(); }); + }, function () { + if ('box' in state) newState.box = state.box; // preserve the latest box state information + + setUpdateInfo(newState); + + notifications.appUpdatesAvailable(pendingNotifications, callback); }); }); } @@ -123,7 +102,7 @@ function checkBoxUpdates(options, callback) { state.box = updateInfo; setUpdateInfo(state); - callback(); + notifications.boxUpdateAvailable(updateInfo, callback); }); }); }