updates: only send email notifications when not auto-updating

fixes #749
This commit is contained in:
Girish Ramakrishnan
2020-12-29 12:43:53 -08:00
parent ff5702efc3
commit 28dee54a39
6 changed files with 169 additions and 69 deletions
+4 -4
View File
@@ -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() %>
<div style="width: 650px; text-align: left;">
<% for (var i = 0; i < apps.length; i++) { -%>
<p>
A new version <%= apps[i].updateInfo.manifest.version %> of the app '<%= apps[i].app.manifest.title %>' installed at <a href="https://<%= apps[i].app.fqdn %>"><%= apps[i].app.fqdn %></a> is available.
The app '<%= apps[i].app.manifest.title %>' installed at <a href="https://<%= apps[i].app.fqdn %>"><%= apps[i].app.fqdn %></a> has an update available.
</p>
<h5>Changelog:</h5>
<h5><%= apps[i].app.manifest.title %> v<%= apps[i].updateInfo.manifest.version %> changes:</h5>
<%- apps[i].changelogHTML %>
<br/>
@@ -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 { %>
<center>
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
<h3>Dear <%= cloudronName %> Admin,</h3>
<div style="width: 650px; text-align: left;">
<p>
Cloudron v<b><%= newBoxVersion %></b> is now available!
</p>
<h5>Changes:</h5>
<ul>
<% for (var i = 0; i < changelogHTML.length; i++) { %>
<li><%- changelogHTML[i] %></li>
<% } %>
</ul>
<br/>
</div>
<div style="font-size: 10px; color: #333333; background: #ffffff;">
Powered by <a href="https://cloudron.io">Cloudron</a>.
</div>
</center>
<% } %>
+50 -12
View File
@@ -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)
};
+43 -5
View File
@@ -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');
+1 -1
View File
@@ -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);
});
});
+26 -47
View File
@@ -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);
});
});
}