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] %>
+ <% } %>
+
+
+
+
+
+
+
+
+
+<% } %>
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);
});
});
}